From e3c68693c9592b777daf3be7f9271bacb8699c9f Mon Sep 17 00:00:00 2001 From: Edwin Lim Date: Wed, 28 Oct 2026 17:39:22 -0700 Subject: [PATCH 1/2] feat(programs): programs own their orchestrator flows and prompts The five orchestrator programs (posthog-integration, error-tracking, feature-flags, metrics, replay-vision) declare their task graph in `flow.ts` with `defineFlow` from the program contract, and keep their task prompts in `agent/.md`. `pnpm bundle:agent-prompts` generates each program's `agent-prompts.ts`, which `ProgramConfig.agentPrompts` loads on first use. `resolveFlow` applies the run's project traits and flags, dropping a node whose `when` fails and splicing its edges into its dependents. The program runner hands the graph to the orchestrator as `RunConfig.taskGraph`, and the prompts as `RunConfig.agentPrompts`. With `wizard-agent-dag` on, the orchestrator loads the bundled prompts, falling back to context-mill's on any failure, and rewires the planner's queue to the typed graph through `QueueStore.replaceDependencies`. Off, it fetches the prompts as before. Either way it logs `orchestrator graph diff` and `orchestrator exclusions differ` where the typed graph and the planner disagree. The prompts match the recorded context-mill release byte for byte, and assemble the same seed and task prompts as the fetched ones. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/wizard-development/SKILL.md | 3 +- package.json | 3 +- scripts/bundle-agent-prompts.no-jest.ts | 50 +++++ src/agent/__tests__/bundled-prompts.test.ts | 100 ++++++++++ src/agent/agent-prompt-loader.ts | 15 ++ src/agent/runner/index.ts | 4 + .../orchestrator/__tests__/task-graph.test.ts | 172 +++++++++++++++++ .../orchestrator/orchestrator-runner.ts | 117 +++++++++++- .../runner/sequence/orchestrator/queue.ts | 50 ++++- .../sequence/orchestrator/task-graph.ts | 154 ++++++++++++++++ src/agent/runner/shared/types.ts | 5 + src/agent/types.ts | 3 + src/programs/README.md | 26 +++ src/programs/__tests__/agent-prompts.test.ts | 39 ++++ src/programs/__tests__/flows.test.ts | 173 ++++++++++++++++++ src/programs/error-tracking/agent-prompts.ts | 26 +++ .../agent/capture-exceptions.md | 116 ++++++++++++ .../error-tracking/agent/configure.md | 173 ++++++++++++++++++ .../error-tracking/agent/credentials.md | 81 ++++++++ src/programs/error-tracking/agent/init.md | 121 ++++++++++++ src/programs/error-tracking/agent/install.md | 54 ++++++ .../error-tracking/agent/link-releases.md | 87 +++++++++ src/programs/error-tracking/agent/report.md | 103 +++++++++++ .../agent/setup-error-tracking.md | 170 +++++++++++++++++ .../error-tracking/agent/test-setup.md | 55 ++++++ src/programs/error-tracking/agent/verify.md | 82 +++++++++ src/programs/error-tracking/agent/wire-ci.md | 109 +++++++++++ src/programs/error-tracking/flow.ts | 37 ++++ src/programs/error-tracking/index.ts | 3 + src/programs/feature-flags/agent-prompts.ts | 16 ++ .../feature-flags/agent/create-flag.md | 48 +++++ src/programs/feature-flags/agent/evaluate.md | 44 +++++ src/programs/feature-flags/agent/init.md | 35 ++++ src/programs/feature-flags/agent/install.md | 31 ++++ src/programs/feature-flags/agent/report.md | 50 +++++ .../agent/setup-feature-flags.md | 66 +++++++ src/programs/feature-flags/flow.ts | 13 ++ src/programs/feature-flags/index.ts | 3 + src/programs/metrics/agent-prompts.ts | 13 ++ .../metrics/agent/instrument-metrics.md | 32 ++++ src/programs/metrics/agent/report.md | 44 +++++ src/programs/metrics/agent/setup-metrics.md | 60 ++++++ src/programs/metrics/agent/verify-sdk.md | 46 +++++ src/programs/metrics/flow.ts | 15 ++ src/programs/metrics/index.ts | 3 + .../posthog-integration/agent-prompts.ts | 27 +++ .../agent/ai-observability.md | 60 ++++++ .../posthog-integration/agent/capture.md | 51 ++++++ .../posthog-integration/agent/dashboard.md | 23 +++ .../agent/error-tracking.md | 36 ++++ .../posthog-integration/agent/identify.md | 62 +++++++ .../posthog-integration/agent/init.md | 38 ++++ .../posthog-integration/agent/install.md | 67 +++++++ .../agent/integrate-posthog.md | 51 ++++++ .../posthog-integration/agent/logs.md | 60 ++++++ .../posthog-integration/agent/report.md | 53 ++++++ .../posthog-integration/agent/review.md | 104 +++++++++++ .../posthog-integration/agent/warehouse.md | 41 +++++ src/programs/posthog-integration/flow.ts | 53 ++++++ src/programs/posthog-integration/index.ts | 3 + src/programs/program-contract/flow.ts | 73 ++++++++ src/programs/program-contract/index.ts | 7 + src/programs/program-contract/program-step.ts | 5 + src/programs/program-runner/run-program.ts | 11 +- src/programs/replay-vision/agent-prompts.ts | 20 ++ .../replay-vision/agent/enable-replay.md | 46 +++++ src/programs/replay-vision/agent/init.md | 33 ++++ src/programs/replay-vision/agent/install.md | 30 +++ src/programs/replay-vision/agent/report.md | 38 ++++ .../agent/scanner-broken-experiences.md | 40 ++++ .../agent/scanner-session-summaries.md | 37 ++++ .../agent/scanner-user-frustration.md | 35 ++++ .../agent/setup-replay-vision.md | 53 ++++++ src/programs/replay-vision/flow.ts | 21 +++ src/programs/replay-vision/index.ts | 3 + 75 files changed, 3824 insertions(+), 7 deletions(-) create mode 100644 scripts/bundle-agent-prompts.no-jest.ts create mode 100644 src/agent/__tests__/bundled-prompts.test.ts create mode 100644 src/agent/runner/sequence/orchestrator/__tests__/task-graph.test.ts create mode 100644 src/agent/runner/sequence/orchestrator/task-graph.ts create mode 100644 src/programs/__tests__/agent-prompts.test.ts create mode 100644 src/programs/__tests__/flows.test.ts create mode 100644 src/programs/error-tracking/agent-prompts.ts create mode 100644 src/programs/error-tracking/agent/capture-exceptions.md create mode 100644 src/programs/error-tracking/agent/configure.md create mode 100644 src/programs/error-tracking/agent/credentials.md create mode 100644 src/programs/error-tracking/agent/init.md create mode 100644 src/programs/error-tracking/agent/install.md create mode 100644 src/programs/error-tracking/agent/link-releases.md create mode 100644 src/programs/error-tracking/agent/report.md create mode 100644 src/programs/error-tracking/agent/setup-error-tracking.md create mode 100644 src/programs/error-tracking/agent/test-setup.md create mode 100644 src/programs/error-tracking/agent/verify.md create mode 100644 src/programs/error-tracking/agent/wire-ci.md create mode 100644 src/programs/error-tracking/flow.ts create mode 100644 src/programs/feature-flags/agent-prompts.ts create mode 100644 src/programs/feature-flags/agent/create-flag.md create mode 100644 src/programs/feature-flags/agent/evaluate.md create mode 100644 src/programs/feature-flags/agent/init.md create mode 100644 src/programs/feature-flags/agent/install.md create mode 100644 src/programs/feature-flags/agent/report.md create mode 100644 src/programs/feature-flags/agent/setup-feature-flags.md create mode 100644 src/programs/feature-flags/flow.ts create mode 100644 src/programs/metrics/agent-prompts.ts create mode 100644 src/programs/metrics/agent/instrument-metrics.md create mode 100644 src/programs/metrics/agent/report.md create mode 100644 src/programs/metrics/agent/setup-metrics.md create mode 100644 src/programs/metrics/agent/verify-sdk.md create mode 100644 src/programs/metrics/flow.ts create mode 100644 src/programs/posthog-integration/agent-prompts.ts create mode 100644 src/programs/posthog-integration/agent/ai-observability.md create mode 100644 src/programs/posthog-integration/agent/capture.md create mode 100644 src/programs/posthog-integration/agent/dashboard.md create mode 100644 src/programs/posthog-integration/agent/error-tracking.md create mode 100644 src/programs/posthog-integration/agent/identify.md create mode 100644 src/programs/posthog-integration/agent/init.md create mode 100644 src/programs/posthog-integration/agent/install.md create mode 100644 src/programs/posthog-integration/agent/integrate-posthog.md create mode 100644 src/programs/posthog-integration/agent/logs.md create mode 100644 src/programs/posthog-integration/agent/report.md create mode 100644 src/programs/posthog-integration/agent/review.md create mode 100644 src/programs/posthog-integration/agent/warehouse.md create mode 100644 src/programs/posthog-integration/flow.ts create mode 100644 src/programs/program-contract/flow.ts create mode 100644 src/programs/replay-vision/agent-prompts.ts create mode 100644 src/programs/replay-vision/agent/enable-replay.md create mode 100644 src/programs/replay-vision/agent/init.md create mode 100644 src/programs/replay-vision/agent/install.md create mode 100644 src/programs/replay-vision/agent/report.md create mode 100644 src/programs/replay-vision/agent/scanner-broken-experiences.md create mode 100644 src/programs/replay-vision/agent/scanner-session-summaries.md create mode 100644 src/programs/replay-vision/agent/scanner-user-frustration.md create mode 100644 src/programs/replay-vision/agent/setup-replay-vision.md create mode 100644 src/programs/replay-vision/flow.ts diff --git a/.claude/skills/wizard-development/SKILL.md b/.claude/skills/wizard-development/SKILL.md index f1ab3bc85..c69b88a49 100644 --- a/.claude/skills/wizard-development/SKILL.md +++ b/.claude/skills/wizard-development/SKILL.md @@ -20,7 +20,8 @@ infrastructure should consume those boundaries. | Concern | Owner | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Framework detection, context, env conventions | [FrameworkConfig](../../../src/programs/program-contract/framework-config.ts) and [framework configs](../../../src/services/frameworks/) | -| Integration instructions and orchestrator flows/tasks | [context-mill](https://github.com/PostHog/context-mill) | +| Integration instructions (skills) | [context-mill](https://github.com/PostHog/context-mill) | +| Orchestrator task graphs and task prompts | Each program's `flow.ts` and `agent/`; see [orchestrator flows](../../../src/programs/README.md#orchestrator-flows) | | Programs: detection, runs, prerequisites and outcomes | One folder per program in [programs](../../../src/programs/), entered through its `index.ts` | | Tools: commands that run no agent | [tools](../../../src/tools/), entered through `@tools`, with their screens in [TUI tools](../../../src/tui/tools/) | | Program screens, flows and decks | [TUI programs](../../../src/tui/programs/) | diff --git a/package.json b/package.json index d190d3613..8093c9ced 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,8 @@ "wizard-ci-replay": "tsx scripts/tui-replay.no-jest.ts", "decisions-detect": "tsx scripts/decisions-detect.no-jest.ts", "check:decisions-bundle": "tsx scripts/check-decisions-bundle.no-jest.ts", - "record:context-mill-fixtures": "tsx scripts/record-context-mill-fixtures.no-jest.ts" + "record:context-mill-fixtures": "tsx scripts/record-context-mill-fixtures.no-jest.ts", + "bundle:agent-prompts": "tsx scripts/bundle-agent-prompts.no-jest.ts" }, "lint-staged": { ".claude/settings.json": "sh -c 'printf \"\\n\\033[31mDo not commit .claude/settings.json — use .claude/settings.local.json (gitignored).\\nUnstage with: git restore --staged .claude/settings.json\\033[0m\\n\\n\" >&2 && exit 1'", diff --git a/scripts/bundle-agent-prompts.no-jest.ts b/scripts/bundle-agent-prompts.no-jest.ts new file mode 100644 index 000000000..6c98acd57 --- /dev/null +++ b/scripts/bundle-agent-prompts.no-jest.ts @@ -0,0 +1,50 @@ +/** + * Generates each program's `agent-prompts.ts` from its `agent/*.md`, so the + * bundle and `pnpm try` both carry the prompts as plain strings. Run with + * `pnpm bundle:agent-prompts` after editing a prompt; a test fails while a + * generated module is stale. + */ + +import { execFileSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const PROGRAMS = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', + 'src', + 'programs', +); + +export const GENERATED_HEADER = + '// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit.'; + +const written: string[] = []; +for (const program of fs.readdirSync(PROGRAMS)) { + const agentDir = path.join(PROGRAMS, program, 'agent'); + if (!fs.existsSync(agentDir)) continue; + const files = fs + .readdirSync(agentDir) + .filter((file) => file.endsWith('.md')) + .sort(); + const entries = files.map( + (file) => + ` ${JSON.stringify(file.replace(/\.md$/, ''))}: ${JSON.stringify( + fs.readFileSync(path.join(agentDir, file), 'utf8'), + )},`, + ); + const out = path.join(PROGRAMS, program, 'agent-prompts.ts'); + fs.writeFileSync( + out, + `${GENERATED_HEADER}\n\n/** This program's agent prompts by task type, verbatim. */\nexport const AGENT_PROMPTS: Readonly> = {\n${entries.join( + '\n', + )}\n};\n`, + ); + written.push(out); +} + +execFileSync('pnpm', ['exec', 'prettier', '--write', ...written], { + stdio: 'inherit', +}); +console.log(`Generated ${written.length} agent-prompt modules.`); diff --git a/src/agent/__tests__/bundled-prompts.test.ts b/src/agent/__tests__/bundled-prompts.test.ts new file mode 100644 index 000000000..c293da0fd --- /dev/null +++ b/src/agent/__tests__/bundled-prompts.test.ts @@ -0,0 +1,100 @@ +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it, vi } from 'vitest'; +import { + assembleSeedPrompt, + assembleTaskPrompt, + bundledRegistry, + loadAgentRegistry, + type OrchestratorPromptContext, +} from '../agent-prompt-loader'; + +const ROOT = path.resolve(__dirname, '../../..'); +const RECORDED = path.join(ROOT, 'fixtures', 'context-mill'); +const BASE_URL = 'https://context-mill.test'; + +// The fetch path reads the recorded release: the menu, then each prompt by its download URL's flow and id. +vi.mock('@shared/fetch-retry', () => ({ + fetchWithRetry: (url: string) => { + const menu = JSON.parse( + fs.readFileSync(path.join(RECORDED, 'agent-menu.json'), 'utf8'), + ) as { agents: { id: string; flow: string; downloadUrl: string }[] }; + const file = + url === `${BASE_URL}/agent-menu.json` + ? path.join(RECORDED, 'agent-menu.json') + : (() => { + const entry = menu.agents.find((a) => a.downloadUrl === url); + if (!entry) throw new Error(`unexpected fetch ${url}`); + return path.join(RECORDED, 'agents', entry.flow, `${entry.id}.md`); + })(); + const text = fs.readFileSync(file, 'utf8'); + return Promise.resolve({ text: () => Promise.resolve(text) }); + }, +})); + +const FLOWS: Array<[string, string]> = [ + ['error-tracking', 'error-tracking'], + ['feature-flags', 'feature-flags'], + ['metrics', 'metrics'], + ['integration-v2', 'posthog-integration'], + ['replay-vision', 'replay-vision'], +]; + +// Run-specific values, normalized so only the prompt text can differ. +const CONTEXT: OrchestratorPromptContext = { + projectId: 1, + projectApiKey: 'phc_test', + host: { + region: 'us', + apiHost: 'https://us.i.posthog.com', + appHost: 'https://us.posthog.com', + assetHost: 'https://us-assets.i.posthog.com', + mcpUrl: 'https://mcp.posthog.com/mcp', + }, + examplePath: '.posthog-wizard-cache/reference/EXAMPLE.md', + commandmentsPath: '.posthog-wizard-cache/reference/COMMANDMENTS.md', + facts: [], +}; + +/** The value, or a failed test naming what was missing. */ +function must(value: T | undefined, what: string): T { + if (value === undefined) throw new Error(`missing ${what}`); + return value; +} + +function programPrompts(program: string): Record { + const dir = path.join(ROOT, 'src', 'programs', program, 'agent'); + return Object.fromEntries( + fs + .readdirSync(dir) + .filter((file) => file.endsWith('.md')) + .map((file) => [ + file.replace(/\.md$/, ''), + fs.readFileSync(path.join(dir, file), 'utf8'), + ]), + ); +} + +describe.each(FLOWS)('bundled %s prompts', (flow, program) => { + it('assemble byte-identically to the fetched ones', async () => { + const fetched = await loadAgentRegistry(BASE_URL, flow); + const bundled = bundledRegistry(programPrompts(program), flow); + + expect(bundled.types.sort()).toEqual(fetched.types.sort()); + expect(bundled.sinkTypes).toEqual(fetched.sinkTypes); + expect(bundled.runnerSeededTypes).toEqual(fetched.runnerSeededTypes); + expect( + assembleSeedPrompt(CONTEXT, must(bundled.seed, 'bundled seed').body), + ).toBe( + assembleSeedPrompt(CONTEXT, must(fetched.seed, 'fetched seed').body), + ); + for (const type of fetched.types) { + const a = must(bundled.get(type), `bundled ${type}`); + const b = must(fetched.get(type), `fetched ${type}`); + expect({ ...a, body: undefined }).toEqual({ ...b, body: undefined }); + expect(assembleTaskPrompt(CONTEXT, a.body, a.skills)).toBe( + assembleTaskPrompt(CONTEXT, b.body, b.skills), + ); + } + }); +}); diff --git a/src/agent/agent-prompt-loader.ts b/src/agent/agent-prompt-loader.ts index d77c688b6..12122d295 100644 --- a/src/agent/agent-prompt-loader.ts +++ b/src/agent/agent-prompt-loader.ts @@ -459,6 +459,21 @@ async function fetchText(url: string): Promise { return res.text(); } +/** The registry for one flow from prompts the wizard bundles, keyed by task type; parsed as the fetched ones are. */ +export function bundledRegistry( + prompts: Readonly>, + flow: string, + opts?: Parameters[2], +): AgentRegistry { + return buildRegistry( + Object.entries(prompts).map(([type, text]) => + parseAgentPrompt(text, type, flow), + ), + flow, + opts, + ); +} + /** * Fetch the agent menu and every agent prompt it lists, parse them, and build * the registry for one flow. Throws if the menu cannot be fetched — the diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index 95ced2ad0..e6d724f57 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -70,6 +70,10 @@ export type { export { DEFAULT_BINDING } from './switchboard'; export type { ProgramBinding } from './switchboard'; export type { TaskOutcome } from './sequence/orchestrator/queue'; +export type { + TaskGraph, + TaskGraphNode, +} from './sequence/orchestrator/task-graph'; /** * Run a program's agent pipeline. diff --git a/src/agent/runner/sequence/orchestrator/__tests__/task-graph.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/task-graph.test.ts new file mode 100644 index 000000000..3a5668843 --- /dev/null +++ b/src/agent/runner/sequence/orchestrator/__tests__/task-graph.test.ts @@ -0,0 +1,172 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +vi.mock('@utils/analytics', () => ({ + analytics: { wizardCapture: vi.fn(), captureException: vi.fn() }, +})); + +import { QueueStore } from '@agent/runner/sequence/orchestrator/queue'; +import { + graphDiff, + graphsAgree, + typedEdges, + type TaskGraph, +} from '@agent/runner/sequence/orchestrator/task-graph'; + +function newStore(): QueueStore { + return new QueueStore( + fs.mkdtempSync(path.join(os.tmpdir(), 'task-graph-')), + 'run', + ); +} + +const CHAIN: TaskGraph = { + sink: 'report', + excluded: [], + nodes: { + install: { dependsOn: [], picked: true }, + init: { dependsOn: [], picked: true }, + capture: { dependsOn: ['install', 'init'] }, + report: { dependsOn: ['capture'] }, + }, +}; + +describe('typedEdges', () => { + it('splices an unqueued dependency into its own dependencies', () => { + const store = newStore(); + const init = store.enqueue({ type: 'init' }); + const capture = store.enqueue({ type: 'capture' }); + const report = store.enqueue({ type: 'report' }); + const edges = typedEdges(store, CHAIN); + expect(edges.get(capture.id)).toEqual([init.id]); + expect(edges.get(report.id)).toEqual([capture.id]); + }); + + it('pairs a fanned-out chain by its inputs', () => { + const graph: TaskGraph = { + sink: 'report', + excluded: [], + nodes: { + verify: { dependsOn: [], picked: true }, + instrument: { + dependsOn: ['verify'], + picked: true, + matchInputs: ['service_name'], + }, + report: { dependsOn: ['instrument'] }, + }, + }; + const store = newStore(); + const api = { service_name: 'api' }; + const worker = { service_name: 'worker' }; + const verifyApi = store.enqueue({ type: 'verify', inputs: api }); + const verifyWorker = store.enqueue({ type: 'verify', inputs: worker }); + const instrumentApi = store.enqueue({ type: 'instrument', inputs: api }); + const instrumentWorker = store.enqueue({ + type: 'instrument', + inputs: worker, + }); + const report = store.enqueue({ type: 'report' }); + const edges = typedEdges(store, graph); + expect(edges.get(instrumentApi.id)).toEqual([verifyApi.id]); + expect(edges.get(instrumentWorker.id)).toEqual([verifyWorker.id]); + expect(edges.get(report.id)).toEqual([ + instrumentApi.id, + instrumentWorker.id, + ]); + }); +}); + +describe('graphDiff', () => { + it('agrees with a planner graph that has redundant edges', () => { + const store = newStore(); + const install = store.enqueue({ type: 'install' }); + const init = store.enqueue({ type: 'init' }); + const capture = store.enqueue({ + type: 'capture', + dependsOn: [install.id, init.id], + }); + store.enqueue({ type: 'report', dependsOn: [capture.id, install.id] }); + expect(graphsAgree(graphDiff(store, CHAIN))).toBe(true); + }); + + it('catches a fanned-out task wired to the wrong sibling', () => { + const graph: TaskGraph = { + sink: 'report', + excluded: [], + nodes: { + verify: { dependsOn: [], picked: true }, + instrument: { + dependsOn: ['verify'], + picked: true, + matchInputs: ['service_name'], + }, + report: { dependsOn: ['instrument'] }, + }, + }; + const store = newStore(); + const api = { service_name: 'api' }; + const worker = { service_name: 'worker' }; + const verifyApi = store.enqueue({ type: 'verify', inputs: api }); + store.enqueue({ type: 'verify', inputs: worker }); + const instrumentWorker = store.enqueue({ + type: 'instrument', + inputs: worker, + dependsOn: [verifyApi.id], + }); + store.enqueue({ type: 'report', dependsOn: [instrumentWorker.id] }); + expect(graphDiff(store, graph).edges).toContainEqual({ + type: 'instrument', + missing: ['verify'], + extra: ['verify'], + }); + }); + + it('names a dropped edge, a missing node and an extra one', () => { + const store = newStore(); + store.enqueue({ type: 'install' }); + store.enqueue({ type: 'report' }); + store.enqueue({ type: 'invented' }); + expect(graphDiff(store, CHAIN)).toEqual({ + missingTypes: ['capture'], + extraTypes: ['invented'], + edges: [{ type: 'report', missing: ['install'], extra: [] }], + }); + }); +}); + +describe('QueueStore.replaceDependencies', () => { + it('rewires the queue to the typed graph', () => { + const store = newStore(); + const install = store.enqueue({ type: 'install' }); + const init = store.enqueue({ type: 'init' }); + const capture = store.enqueue({ type: 'capture' }); + const report = store.enqueue({ type: 'report' }); + expect(store.replaceDependencies(typedEdges(store, CHAIN))).toEqual({ + applied: true, + }); + expect(store.get(capture.id)?.dependsOn).toEqual([install.id, init.id]); + expect(store.get(report.id)?.dependsOn).toEqual([capture.id]); + expect(graphsAgree(graphDiff(store, CHAIN))).toBe(true); + }); + + it('refuses a cycle and changes nothing', () => { + const store = newStore(); + const a = store.enqueue({ type: 'a' }); + const b = store.enqueue({ type: 'b', dependsOn: [a.id] }); + const result = store.replaceDependencies(new Map([[a.id, [b.id]]])); + expect(result).toEqual({ applied: false, reason: 'cycle' }); + expect(store.get(a.id)?.dependsOn).toEqual([]); + }); + + it('refuses to rewire a task that already started', () => { + const store = newStore(); + const a = store.enqueue({ type: 'a' }); + const b = store.enqueue({ type: 'b' }); + store.start(b.id); + expect(store.replaceDependencies(new Map([[b.id, [a.id]]])).applied).toBe( + false, + ); + }); +}); diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 76e5a5d87..4769c9d56 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -86,17 +86,25 @@ import { RunMetrics } from './run-metrics'; import { dependencyClosure, uncoveredBySink } from './queue-tools'; import { deferSeededTasks } from './seeded-deps'; import { createSkillPicker } from './skill-picks'; +import { + graphDiff, + graphsAgree, + typedEdges, + type TaskGraph, +} from './task-graph'; import { LONGER_ASK_TIMEOUT_MS, shouldDisableAsk } from '@shared/ask-policy'; import { agentRunTools, assembleSeedPrompt, assembleTaskPrompt, + bundledRegistry, loadAgentRegistry, promptModelFor, resolveTask, taskModelSpec, ASK_TOOL, type AgentPrompt, + type AgentRegistry, type OrchestratorPromptContext, } from '../../../agent-prompt-loader'; @@ -586,6 +594,73 @@ export function effectiveExcludedTaskTypes( ]; } +/** The caller's typed graph, or undefined when it has none or building it failed. */ +function readTaskGraph( + config: SequenceContext['config'], +): TaskGraph | undefined { + try { + return config.taskGraph?.(); + } catch (error) { + logToFile(`[orchestrator] typed graph unavailable: ${String(error)}`); + return undefined; + } +} + +/** Logs where the graph's conditions and the program's exclusion list disagree. */ +function compareExclusions( + flow: string, + excludedTypes: readonly string[], + graph: TaskGraph, +): void { + const ci = new Set(ciExcludedTaskTypes()); + const program = excludedTypes.filter((t) => !ci.has(t)).sort(); + const typed = [...graph.excluded].sort(); + if (program.join(',') === typed.join(',')) return; + logToFile( + `[orchestrator] exclusions differ: program=${program.join( + ',', + )} graph=${typed.join(',')}`, + ); + analytics.wizardCapture('orchestrator exclusions differ', { + flow, + program_excluded: program, + graph_excluded: typed, + }); +} + +/** The flow's prompts: the caller's bundle when `wizard-agent-dag` is on, falling back to context-mill's on any failure. */ +async function loadRegistry( + config: SequenceContext['config'], + boot: SequenceContext['boot'], + flow: string, + options: Parameters[2], +): Promise { + if (config.agentPrompts && isAgentDagEnabled(boot.wizardFlags)) { + try { + const bundled = bundledRegistry( + await config.agentPrompts(), + flow, + options, + ); + if (bundled.seed) { + analytics.wizardCapture('orchestrator agent prompts loaded', { + source: 'bundle', + flow, + }); + return bundled; + } + logToFile(`[orchestrator] bundled prompts for ${flow} have no seed`); + } catch (error) { + logToFile(`[orchestrator] bundled prompts failed: ${String(error)}`); + } + analytics.wizardCapture('orchestrator agent prompts loaded', { + source: 'fallback', + flow, + }); + } + return loadAgentRegistry(boot.skillsBaseUrl, flow, options); +} + export async function runOrchestrator( context: SequenceContext, ): Promise { @@ -650,15 +725,25 @@ async function executeOrchestrator( // once up front: its types drive enqueue validation, and resolving a task to // its run config is then synchronous, with no mid-drain network latency. const flow = config.agentFlow ?? programId; - const registry = await loadAgentRegistry(boot.skillsBaseUrl, flow, { - exclude: effectiveExcludedTaskTypes(config, boot.wizardFlags), + const taskGraph = readTaskGraph(config); + // The typed graph decides with `wizard-agent-dag` on; off, it only compares against the planner's. + const typedGraph = + taskGraph !== undefined && isAgentDagEnabled(boot.wizardFlags); + const excludedTypes = effectiveExcludedTaskTypes(config, boot.wizardFlags); + if (taskGraph) compareExclusions(flow, excludedTypes, taskGraph); + const registryOptions = { + exclude: + typedGraph && taskGraph + ? [...ciExcludedTaskTypes(), ...taskGraph.excluded] + : excludedTypes, // Baked into the prompts at load, so enqueue, dispatch, and telemetry all read one effective spec. overrides: resolveStageOverrides( programId, boot.wizardFlags, boot.wizardFlagPayloads, ), - }); + }; + const registry = await loadRegistry(config, boot, flow, registryOptions); if (signal?.aborted) return cancelledRun(); const seedPrompt = registry.seed; if (!seedPrompt) { @@ -1120,6 +1205,32 @@ async function executeOrchestrator( } if (deferred.length > 0) renderQueue(); + if (taskGraph) { + const diff = graphDiff(store, taskGraph); + const agree = graphsAgree(diff); + logToFile( + `[orchestrator] typed graph ${agree ? 'matches' : 'differs'} (${ + typedGraph ? 'decides' : 'shadow' + })${agree ? '' : ` ${JSON.stringify(diff)}`}`, + ); + analytics.wizardCapture('orchestrator graph diff', { + flow, + deciding: typedGraph ? 'typed' : 'planner', + agree, + missing_types: diff.missingTypes, + extra_types: diff.extraTypes, + edge_diff_types: diff.edges.map((e) => e.type), + }); + if (typedGraph && !agree) { + const rewired = store.replaceDependencies(typedEdges(store, taskGraph)); + if (rewired.applied) renderQueue(); + else + logToFile( + `[orchestrator] typed graph not applied: ${rewired.reason ?? ''}`, + ); + } + } + // Canary for the one-way rule. The enqueue guard rejects the edge outright, so // this should never fire — but if it ever does, the seeded task has been // pulled back toward the front of the drain and its prompt lands mid-run diff --git a/src/agent/runner/sequence/orchestrator/queue.ts b/src/agent/runner/sequence/orchestrator/queue.ts index 24746c1e4..322cf9a4c 100644 --- a/src/agent/runner/sequence/orchestrator/queue.ts +++ b/src/agent/runner/sequence/orchestrator/queue.ts @@ -103,7 +103,9 @@ export interface QueuedTask { * still-pending task, for a runner-seeded task that was queued before the * planner ran and so could not name its dependencies then. It is additive, * pending-only, and cycle-checked per edge, so the DAG property above still - * holds. Nothing else mutates this field. + * holds. The other is {@link QueueStore.replaceDependencies}, which swaps in a + * program's typed graph whole, before the drain, and refuses any cycle. + * Nothing else mutates this field. */ dependsOn: string[]; inputs: Record; @@ -350,6 +352,38 @@ export class QueueStore { return { added, refused }; } + /** + * Replace every listed task's dependencies at once, with a program's typed + * graph. All or nothing: refused when a changed task is no longer pending, + * an id is unknown, or the result has a cycle. + */ + replaceDependencies(edges: ReadonlyMap): { + applied: boolean; + reason?: string; + } { + const next = new Map(this.tasks.map((t) => [t.id, t.dependsOn])); + for (const [id, deps] of edges) { + const task = this.get(id); + if (!task) return { applied: false, reason: `unknown task ${id}` }; + const same = + deps.length === task.dependsOn.length && + deps.every((d) => task.dependsOn.includes(d)); + if (same) continue; + if (task.status !== TaskStatus.Pending) { + return { applied: false, reason: `${task.type} already started` }; + } + if (deps.some((d) => d === id || !this.get(d))) { + return { applied: false, reason: `bad dependency for ${task.type}` }; + } + next.set(id, [...deps]); + } + if (hasCycle(next)) return { applied: false, reason: 'cycle' }; + for (const task of this.tasks) + task.dependsOn = [...(next.get(task.id) ?? [])]; + this.reflect(); + return { applied: true }; + } + start(id: string): QueuedTask { const t = this.require(id); t.status = TaskStatus.Running; @@ -471,3 +505,17 @@ export class QueueStore { return t; } } + +function hasCycle(edges: ReadonlyMap): boolean { + const state = new Map(); + const visit = (id: string): boolean => { + const seen = state.get(id); + if (seen === 'done') return false; + if (seen === 'open') return true; + state.set(id, 'open'); + for (const dep of edges.get(id) ?? []) if (visit(dep)) return true; + state.set(id, 'done'); + return false; + }; + return [...edges.keys()].some(visit); +} diff --git a/src/agent/runner/sequence/orchestrator/task-graph.ts b/src/agent/runner/sequence/orchestrator/task-graph.ts new file mode 100644 index 000000000..3453f1b00 --- /dev/null +++ b/src/agent/runner/sequence/orchestrator/task-graph.ts @@ -0,0 +1,154 @@ +/** + * A program's typed task graph, as the orchestrator receives it: resolved by + * the caller, with its conditions already applied. The runner compares it to + * the planner's graph, and when the flow's flag says so, rewires the queue to + * it. Pure and store-reading only; nothing here knows a task type. + */ +import type { QueuedTask, QueueStore } from './queue'; + +export interface TaskGraphNode { + /** The types this one runs after. A type with no queued task stands in for its own dependencies. */ + dependsOn: readonly string[]; + /** Match each dependency to the tasks of its type that share these input values, for a fanned-out chain. */ + matchInputs?: readonly string[]; + /** The graph does not decide whether it is queued: the planner or the caller does. */ + picked?: boolean; +} + +export interface TaskGraph { + /** The task that runs last and reports on the run. */ + sink: string; + nodes: Readonly>; + /** Types the graph's conditions excluded from this run. */ + excluded: readonly string[]; +} + +/** How the planner's graph differs from the typed one, by type. Empty lists everywhere means they agree. */ +export interface GraphDiff { + /** Types the graph always queues that the planner did not. */ + missingTypes: string[]; + /** Queued types the graph does not have, or excluded. */ + extraTypes: string[]; + /** Per queued task, the types of the ancestor tasks one graph has and the other lacks. */ + edges: { type: string; missing: string[]; extra: string[] }[]; +} + +/** The node's dependency types, each absent type replaced by its own, recursively. */ +function presentDependencyTypes( + graph: TaskGraph, + type: string, + present: ReadonlySet, + seen: Set = new Set(), +): string[] { + const out: string[] = []; + for (const dep of graph.nodes[type]?.dependsOn ?? []) { + if (seen.has(dep)) continue; + seen.add(dep); + if (present.has(dep)) out.push(dep); + else out.push(...presentDependencyTypes(graph, dep, present, seen)); + } + return [...new Set(out)]; +} + +function sharesInputs( + a: QueuedTask, + b: QueuedTask, + keys: readonly string[] | undefined, +): boolean { + return (keys ?? []).every( + (key) => + a.inputs[key] === undefined || + b.inputs[key] === undefined || + a.inputs[key] === b.inputs[key], + ); +} + +/** Each queued task's dependencies under the typed graph. A task whose type the graph lacks keeps its own. */ +export function typedEdges( + store: QueueStore, + graph: TaskGraph, +): Map { + const tasks = store.list(); + const present = new Set(tasks.map((t) => t.type)); + const edges = new Map(); + for (const task of tasks) { + const node = graph.nodes[task.type]; + if (!node) { + edges.set(task.id, [...task.dependsOn]); + continue; + } + const depTypes = new Set(presentDependencyTypes(graph, task.type, present)); + edges.set( + task.id, + tasks + .filter( + (dep) => + dep.id !== task.id && + depTypes.has(dep.type) && + sharesInputs(task, dep, node.matchInputs), + ) + .map((dep) => dep.id), + ); + } + return edges; +} + +/** Every task id `id` runs after, transitively. */ +function ancestorIds( + id: string, + edgesOf: (id: string) => readonly string[], +): Set { + const seen = new Set(); + const visit = (current: string): void => { + for (const dep of edgesOf(current)) { + if (seen.has(dep)) continue; + seen.add(dep); + visit(dep); + } + }; + visit(id); + return seen; +} + +/** The planner's graph against the typed one. Ancestors, not direct edges, so a redundant edge is no difference. */ +export function graphDiff(store: QueueStore, graph: TaskGraph): GraphDiff { + const tasks = store.list(); + const present = new Set(tasks.map((t) => t.type)); + const excluded = new Set(graph.excluded); + const typed = typedEdges(store, graph); + const typeOf = (id: string) => store.get(id)?.type; + + const missingTypes = Object.entries(graph.nodes) + .filter(([type, node]) => !node.picked && !present.has(type)) + .map(([type]) => type); + const extraTypes = [...present].filter( + (type) => !graph.nodes[type] || excluded.has(type), + ); + const edges: GraphDiff['edges'] = []; + for (const task of tasks) { + if (!graph.nodes[task.type]) continue; + const planned = ancestorIds( + task.id, + (id) => store.get(id)?.dependsOn ?? [], + ); + const expected = ancestorIds(task.id, (id) => typed.get(id) ?? []); + // By task, so a fanned-out chain wired to the wrong sibling counts; reported by type. + const typesOf = (ids: string[]) => + [...new Set(ids.flatMap((id) => typeOf(id) ?? []))].sort(); + const missing = typesOf([...expected].filter((id) => !planned.has(id))); + const extra = typesOf([...planned].filter((id) => !expected.has(id))); + if (missing.length > 0 || extra.length > 0) { + edges.push({ type: task.type, missing, extra }); + } + } + return { missingTypes, extraTypes, edges }; +} + +/** Whether a diff found nothing. */ +export function graphsAgree(diff: GraphDiff): boolean { + return ( + diff.missingTypes.length === 0 && + diff.extraTypes.length === 0 && + diff.edges.length === 0 + ); +} diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index 23d07e408..bc28a61aa 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -17,6 +17,7 @@ import type { ApiProject, ApiUser } from '@shared/api'; import type { Harness, Integration, Sequence } from '@shared/constants'; import type { ErrorCode } from '@shared/errors'; import type { TraitTags } from '@shared/skill-resolution'; +import type { TaskGraph } from '../sequence/orchestrator/task-graph'; import type { LLMProvider } from '@posthog/warlock'; import type { AgentInteraction, ProgressEmitter } from '../../progress'; import type { EffortLevel } from '../switchboard/models'; @@ -220,6 +221,10 @@ export interface ResolvedRunConfig { projectFacts?: () => readonly string[]; /** The run's project traits as tags, which skill criteria require or prefer. Read when the orchestrator starts. */ traitTags?: () => TraitTags; + /** The flow's typed task graph, resolved by the caller from the run's traits and flags. */ + taskGraph?: () => TaskGraph; + /** The flow's agent prompts by task type, bundled by the caller; the orchestrator fetches them from context-mill otherwise. */ + agentPrompts?: () => Promise>>; /** Completion hooks, bound by the caller. */ hooks?: RunHooks; scanReport?: 'flush' | 'defer'; // defer leaves the scan report to the outer run diff --git a/src/agent/types.ts b/src/agent/types.ts index 1b5e7aacb..2108f6924 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -35,5 +35,8 @@ export type { ProgramBinding, AgentRouting } from './runner'; /** What `RunHooks.recordTaskOutcomes` receives: each orchestrated task's final state. */ export type { TaskOutcome } from './runner'; +/** A program's typed task graph, as `RunConfig.taskGraph` hands it to the orchestrator. */ +export type { TaskGraph, TaskGraphNode } from './runner'; + /** One streamed piece of an MCP prompt run: text, a tool call or result, an error, or the end. */ export type { McpPromptChunk } from './mcp-prompt-streaming'; diff --git a/src/programs/README.md b/src/programs/README.md index 8f078a27e..a6f8d7df3 100644 --- a/src/programs/README.md +++ b/src/programs/README.md @@ -98,6 +98,7 @@ reaches the UI. | One program's config, detection and prompt | Its own folder, such as [`metrics`](metrics/index.ts) | | One program's flow, deck and screens | `src/tui/programs//`, registered in [`src/tui/programs/index.ts`](../tui/programs/index.ts) | | One program's e2e test definition | Its folder's `test/e2e.json`, read by [`e2e-harness/profiles.ts`](../../e2e-harness/profiles.ts) | +| One orchestrator program's task graph and prompts | Its folder's `flow.ts` and `agent/.md`; see [orchestrator flows](#orchestrator-flows) | | Code several programs share | [`shared`](shared/skill-program.ts): program builders and [`program-scopes.ts`](shared/program-scopes.ts) | | The list of every program | [`program-registry.ts`](program-runner/program-registry.ts) | | Running one program, and its input and outcome | [`run-program.ts`](program-runner/run-program.ts), [`detect-program.ts`](program-runner/detect-program.ts) and [`program-input.ts`](program-runner/program-input.ts) | @@ -121,6 +122,31 @@ program's project references only the shared program code, so an import of a sibling program or the registry fails with TS6307; see [layer boundaries](../../.claude/skills/wizard-development/references/ARCHITECTURE.md#layer-boundaries). +## Orchestrator flows + +An orchestrator program declares its task graph in `flow.ts` with `defineFlow` +from the program contract: one node per task type, keyed by type, with +`dependsOn`, an optional `when` over the run's project traits and flags, +`picked` for a node the planner or the program decides to queue, and +`matchInputs` for a fanned-out chain. `resolveFlow` applies the conditions; a +dropped node's dependents inherit its edges. The run hands the result to the +orchestrator as `RunConfig.taskGraph`. A `when` treats a missing trait as "run +the node". The run's traits come from +[`@services/project-traits`](../services/README.md). + +Each task's prompt is `agent/.md`, the job to be done; the skills it +names carry the implementation detail. `pnpm bundle:agent-prompts` regenerates +the program's `agent-prompts.ts` from that folder, which `agentPrompts` loads on +first use. Tests in [`__tests__`](__tests__) fail when the module is stale, when +a prompt differs from the recorded context-mill release in +`fixtures/context-mill/`, when a graph's nodes differ from its prompts, or when +a node misses the sink. + +With `wizard-agent-dag` on, the orchestrator loads the bundled prompts (falling +back to context-mill's on any failure) and rewires the planner's queue to the +typed graph. Off, it fetches the prompts and logs where the typed graph and the +planner's graph differ as `orchestrator graph diff`. + ## Session store and task streams The session store and the task stream that pushes it to PostHog are services. diff --git a/src/programs/__tests__/agent-prompts.test.ts b/src/programs/__tests__/agent-prompts.test.ts new file mode 100644 index 000000000..64017c47f --- /dev/null +++ b/src/programs/__tests__/agent-prompts.test.ts @@ -0,0 +1,39 @@ +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it } from 'vitest'; +import { PROGRAM_REGISTRY } from '../program-runner/program-registry'; + +const PROGRAMS = path.resolve(__dirname, '..'); +const RECORDED = path.resolve( + __dirname, + '../../../fixtures/context-mill/agents', +); + +// Every program that bundles prompts, with the context-mill flow they came from. +const BUNDLES = PROGRAM_REGISTRY.filter((c) => c.agentPrompts).map( + (c) => [c.id, c.agentFlow ?? c.id, c] as const, +); + +function readPrompts(dir: string): Record { + return Object.fromEntries( + fs + .readdirSync(dir) + .filter((file) => file.endsWith('.md')) + .map((file) => [ + file.replace(/\.md$/, ''), + fs.readFileSync(path.join(dir, file), 'utf8'), + ]), + ); +} + +describe.each(BUNDLES)('%s agent prompts', (program, flow, config) => { + const own = readPrompts(path.join(PROGRAMS, program, 'agent')); + + it('bundles its agent folder exactly (run `pnpm bundle:agent-prompts`)', async () => { + expect(await config.agentPrompts?.()).toEqual(own); + }); + + it('matches the recorded context-mill release byte for byte', () => { + expect(own).toEqual(readPrompts(path.join(RECORDED, flow))); + }); +}); diff --git a/src/programs/__tests__/flows.test.ts b/src/programs/__tests__/flows.test.ts new file mode 100644 index 000000000..057a0fab8 --- /dev/null +++ b/src/programs/__tests__/flows.test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it } from 'vitest'; +import { WIZARD_DEFAULT_AIO_LOGS_FLAG_KEY } from '@shared/constants'; +import { PROGRAM_REGISTRY } from '../program-runner/program-registry'; +import { + resolveFlow, + type FlowContext, + type ProgramConfig, +} from '@programs/program-contract'; + +/** The frontmatter fields the graph must agree with. */ +function parseAgentPromptFrontmatter(text: string, type: string) { + const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? ''; + const flag = (key: string) => + new RegExp(`^${key}:\\s*true\\b`, 'm').test(frontmatter); + return { + type, + seed: flag('seed'), + sink: flag('sink'), + runnerSeeded: flag('runnerSeeded'), + }; +} + +const NO_TRAITS: FlowContext = { traits: {}, flags: {} }; + +const FLOW_PROGRAMS = PROGRAM_REGISTRY.filter( + (config): config is ProgramConfig & Required> => + config.flow !== undefined, +); + +/** Every type `type` runs after, transitively. */ +function ancestors( + nodes: Readonly>, + type: string, +): Set { + const seen = new Set(); + const visit = (t: string) => { + for (const dep of nodes[t]?.dependsOn ?? []) { + if (seen.has(dep)) continue; + seen.add(dep); + visit(dep); + } + }; + visit(type); + return seen; +} + +it('covers the five orchestrator flows', () => { + expect(FLOW_PROGRAMS.map((c) => c.id).sort()).toEqual([ + 'error-tracking', + 'feature-flags', + 'metrics', + 'posthog-integration', + 'replay-vision', + ]); +}); + +describe.each(FLOW_PROGRAMS.map((c) => [c.id, c] as const))( + '%s flow', + (_, config) => { + const graph = resolveFlow(config.flow, NO_TRAITS); + + it('has one node per task prompt, and the prompts mark the same sink', async () => { + const prompts = Object.entries((await config.agentPrompts?.()) ?? {}).map( + ([type, text]) => parseAgentPromptFrontmatter(text, type), + ); + const tasks = prompts.filter((p) => !p.seed); + expect(Object.keys(config.flow.nodes).sort()).toEqual( + tasks.map((p) => p.type).sort(), + ); + expect(tasks.filter((p) => p.sink).map((p) => p.type)).toEqual([ + config.flow.sink, + ]); + }); + + it('is acyclic and every node reaches the sink', () => { + for (const type of Object.keys(graph.nodes)) { + expect(ancestors(graph.nodes, type).has(type)).toBe(false); + } + const reported = ancestors(graph.nodes, graph.sink); + for (const type of Object.keys(graph.nodes)) { + if (type !== graph.sink) expect(reported).toContain(type); + } + }); + + it('lets nothing but the sink wait on a runner-seeded task', async () => { + const seeded = Object.entries((await config.agentPrompts?.()) ?? {}) + .map(([type, text]) => parseAgentPromptFrontmatter(text, type)) + .filter((p) => p.runnerSeeded) + .map((p) => p.type); + for (const type of seeded) { + expect(graph.nodes[type].picked).toBe(true); + for (const other of Object.keys(graph.nodes)) { + if (other === graph.sink || other === type) continue; + expect(ancestors(graph.nodes, other)).not.toContain(type); + } + } + }); + + it('excludes what the program excludes, for every AIO/Logs flag value', () => { + for (const value of [undefined, 'true', 'false', 'banana']) { + const flags: Record = + value === undefined + ? {} + : { [WIZARD_DEFAULT_AIO_LOGS_FLAG_KEY]: value }; + expect( + [...resolveFlow(config.flow, { traits: {}, flags }).excluded].sort(), + ).toEqual([...(config.excludedTaskTypes?.(flags) ?? [])].sort()); + } + }); + }, +); + +describe('posthog-integration flow', () => { + const found = FLOW_PROGRAMS.find((c) => c.id === 'posthog-integration'); + if (!found) throw new Error('posthog-integration has no flow'); + const config = found; + const graph = resolveFlow(config.flow, NO_TRAITS); + const after = (type: string) => ancestors(graph.nodes, type); + + it('has the shape the integrate-posthog planner prompt describes', () => { + expect(after('identify')).toEqual(new Set(['install', 'init'])); + expect(after('error-tracking')).toEqual(new Set(['install', 'init'])); + expect(after('capture')).toEqual(new Set(['install', 'init', 'identify'])); + expect(after('ai-observability')).toContain('capture'); + expect(after('ai-observability')).not.toContain('error-tracking'); + expect(after('logs')).toContain('ai-observability'); + expect(after('dashboard')).toEqual(after('capture').add('capture')); + for (const type of [ + 'install', + 'init', + 'identify', + 'capture', + 'error-tracking', + 'ai-observability', + 'logs', + ]) { + expect(after('review')).toContain(type); + } + expect(after('report')).toEqual( + new Set(Object.keys(graph.nodes).filter((t) => t !== 'report')), + ); + }); + + it('puts the warehouse step after every task but the report', () => { + expect(after('warehouse')).toEqual( + new Set( + Object.keys(graph.nodes).filter( + (t) => t !== 'report' && t !== 'warehouse', + ), + ), + ); + }); + + it('drops AI Observability on a definite no, and Logs then follows capture', () => { + const judged = resolveFlow(config.flow, { + traits: { callsLlmApi: { value: 'no', source: 'static' } }, + flags: {}, + }); + expect(judged.excluded).toEqual(['ai-observability']); + expect(judged.nodes.logs.dependsOn).toEqual(['capture']); + expect(judged.nodes.review.dependsOn).not.toContain('ai-observability'); + }); + + it('keeps AI Observability when the trait is absent or yes', () => { + expect(resolveFlow(config.flow, NO_TRAITS).excluded).toEqual([]); + expect( + resolveFlow(config.flow, { + traits: { callsLlmApi: { value: 'yes', source: 'static' } }, + flags: {}, + }).excluded, + ).toEqual([]); + }); +}); diff --git a/src/programs/error-tracking/agent-prompts.ts b/src/programs/error-tracking/agent-prompts.ts new file mode 100644 index 000000000..90ec859a8 --- /dev/null +++ b/src/programs/error-tracking/agent-prompts.ts @@ -0,0 +1,26 @@ +// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. + +/** This program's agent prompts by task type, verbatim. */ +export const AGENT_PROMPTS: Readonly> = { + 'capture-exceptions': + "---\ntype: capture-exceptions\nflow: error-tracking\nlabel: Wire up exception capture\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-error-tracking-step, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nMake the errors the app does not catch reach PostHog, by whatever means the\nSDK offers for that. Which means depends on the SDK: some autocapture\nexceptions once you enable it at init, some wire into the framework's own\nerror handler, some give you a boundary to mount at the app entry. Follow the\ndocs and the reference example for this one, and set it up once per runtime —\nnever manual capture calls sprinkled across files.\n\nAn app that runs code on a server as well as in the browser has two runtimes,\nand each needs its own capture. Cover every runtime the docs cover for this\nframework and the app actually uses: the server side takes the SDK's server\nlibrary, where the framework hands you its errors (for Next.js,\n`onRequestError` in `instrumentation.ts`). A browser-only setup leaves every\nserver error uncaptured. An Electron app has the same split: its main process\nruns Node, and its uncaught errors never reach the renderer's capture, so the\nmain process takes the server library. A packaged app ships without the\ndeveloper's `.env`, so a runtime `process.env` lookup in the main process is\nalways empty there: read the token the way the bundler inlines it at build time\n(electron-vite: `import.meta.env.MAIN_VITE_*`). A fully static app has no\nserver runtime to cover.\n\nA page often shows two setups side by side: a global initializer, and an\nexplicit client you hold. Follow one of them the whole way through. Taking the\nwiring from one and the calls from the other reads fine and compiles nowhere —\nan initializer that hands back nothing leaves you nothing to call methods on.\n\nThe SDK is installed and initialized — either it already was, or the install\nand init tasks before you did it (see their handoffs); build on that, do not\nre-check it.\n\nInstall nothing except the server library your server-side capture imports,\nwhen the manifest lacks it: add it with the project's package manager\n(`detect_package_manager`). When it answers `No lockfile found. Default to\nnpm`, look up the tree first: in a workspace package, use the manager whose\nlockfile sits at the workspace root above this directory, and npm only when no\nparent holds a lockfile. Do not run tests or start the app, and do not touch\nthe build config: when the flow includes a configure task, it owns those files.\nStay inside this project's directory and set up capture in those places; that\nis the whole job.\n\nEach import needs its declaration in the manifest of the package that holds the\nimporting file. An app inside a workspace often nests its own package (a\n`v2/package.json` below this directory): when the SDK sits in a different\nmanifest than the code that imports it, add it to the importing package too,\nwith the workspace's manager. Otherwise the import resolves only through\nhoisting and breaks in a focused install or a container build.\n\nOnce your edits are in, run the project's build, or its typecheck when that\ncovers your files and is quicker, and fix what your edits broke. A failure\nthat was there before your edits is not yours: note it in your handoff and\nleave it. Only a build the project defines in this checkout counts: a package\nscript or a compiler. Most Python, Ruby, and PHP apps have none, and then\nthere is nothing to run: do not create environments, install packages, or\nreach for Docker to check your edit. Let the build check API names and types: write from the docs and\nbuild, and open an SDK's type declarations only to fix an error the build\nreports.\n\nBuild, never run: no command that installs or launches the app (`expo run:*`,\n`react-native run-*`, `flutter run`), because it opens a simulator or an\nemulator on the user's machine. Leave no build output the project does not\nignore: in Go, check with `go vet ./...`, because `go build ./...` writes a\nmain package's binary into the project root. Your shell gets a reduced\nenvironment, so a build that cannot find an SDK (`SDK location not found`)\nfailed because of this shell, not your edit: do not retry it, and say so in\nyour handoff.\n\n## Do not copy the SDK's own listeners\n\nException autocapture already registers the global handlers: `window.onerror`\nand `unhandledrejection` in the browser, `uncaughtException` and\n`unhandledRejection` in Node, `sys.excepthook` in Python, the panic hook in\nRust, and the uncaught-exception and signal handlers on iOS and Android. Turn\nthat on with the SDK's own option — `capture_exceptions`,\n`enableExceptionAutocapture`, `enable_exception_autocapture`, `capture_panics`,\n`errorTrackingConfig.autoCapture` — and register none of those handlers\nyourself. A second listener on the same event sends every error twice, and it\nstops matching the SDK's handling the moment the SDK changes.\n\nWhat the SDK cannot see is yours to add: errors a framework catches before any\nglobal handler fires. Express error middleware, Fastify `setErrorHandler`, Hono\n`onError`, Vue `app.config.errorHandler`, Angular `ErrorHandler`, SvelteKit\n`handleError`, a React error boundary, Next.js `global-error` — hook those,\nbecause the framework swallows the error and the global listener never hears\nof it. NestJS is one more layer up: its exception filters answer every request\nerror before Express error middleware runs, so capture goes in a global filter\n(the app's existing `@Catch()` filter when it has one), not in\n`setupExpressErrorHandler`. A platform with no autocapture at all, such as Go,\nis the other case: there the capture boundary at the entry point is the\nmechanism, not a copy of one.\n\n## How you know you succeeded\n\nAn error the app does not catch reaches PostHog from every runtime the app\nruns in, through the mechanism each SDK gives you rather than one you invented,\nand every PostHog library in the manifest is imported. No global error listener\nof your own sits beside the SDK's autocapture. The project builds as well as it\ndid before your edits. You installed nothing but the server library your\ncapture imports, and did not run tests, search outside the project, or read\nthrough the whole app or hand-wrap individual components or routes. Your handoff names the files you changed, the capture mechanism, and\nthe build command you ran with its result, so the report can explain it to the\nuser.\n", + configure: + "---\ntype: configure\nflow: error-tracking\nlabel: Apply build-config changes\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, check_env_keys]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture-exceptions]\n---\n\n## Goal\n\nMake this project's production build emit and upload source maps or debug\nsymbols. Flutter Android/iOS release builds upload Dart AOT symbols separately\nfrom platform-native symbols. Install the skill your task input names\n(`install_skill` with the `skillId`) and read it — it is the source of truth\nfor the per-framework build-config and the uploader wiring.\n\nTwo of the skill's steps are yours:\n\n- **\"Apply build-config changes\"** — make the bundler / build-config edits the\n skill instructs for this platform, so the build produces matching maps or\n debug symbols and runs the uploader. Only web source maps need chunk IDs.\n- **\"Make credentials available at build time\"** — do the skill's step so the\n build can read the upload credentials from the environment. If it calls for a\n loader (e.g. `dotenv`), install it SILENTLY with the project's package\n manager. Skip this step entirely when the platform already auto-loads `.env`.\n\nInstall every dependency with the project's own package manager: call\n`detect_package_manager` before the first install and use its answer,\ntranslating any `npm install` the skill or docs show (`pnpm add -D …`,\n`yarn add -D …`). In a pnpm or yarn workspace, npm fails outright on\n`workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) — that error means the\nwrong manager, never a flag to retry with. When `detect_package_manager`\nanswers `No lockfile found. Default to npm`, look up the tree first: in a\nworkspace package, use the manager whose lockfile sits at the workspace root\nabove this directory, and npm only when no parent holds a lockfile.\n\nChange only what the upload needs. Do not remove or weaken a source-map setting\nthe project already has (`productionBrowserSourceMaps`, Vite's\n`build.sourcemap`, webpack's `devtool`): a build without the upload credentials\nmust still emit what it emitted before. Do not upgrade a package the project\nalready depends on to quiet a peer-dependency warning, because a warning is not\na failure. Upgrade one only when the install or build fails on it, or when the\nskill requires a newer version for the feature you are wiring. Keep the\nproject's version style: an exact pin stays exact.\n\nWire the upload into a build the project already has, in this directory or in\na package nested below it. Code that no build compiles or bundles runs as\nwritten, and its stack traces are already readable: do not add a bundler, a\ncompiler, or a new `build` or `start` script to create one. When this project\nhas no production build to attach an upload to, report this task as\n`not needed` and say why in your handoff.\n\nWhen the upload step you wire runs a machine-global `posthog-cli` rather than\none from the project's own dependencies, do not try to install it or check for\nit: a global install, `posthog-cli` itself, and `command -v` are blocked for\nyou, and the wizard already tried the install before your task. Name it in\nyour handoff as a follow-up for every other machine that runs the release\nbuild — `npm install -g @posthog/cli@latest` — because without it the upload\nstep cannot run.\n\nWhen your changes make the build emit a bundle to a new directory (`dist/`,\n`build/`), check that some script actually runs that output. A project whose\n`build` writes `dist/index.js` while `start` still runs the original source\nnever executes the bundle the maps were uploaded for, so every uploaded map\ngoes unused. Add or fix the script that serves the built output.\n\nThat run script has its own environment to satisfy, and it is not the build's.\nThe uploader's credentials are handled — your build step passes them itself. The\nseparate question is the variables **the application reads when it starts**: the\nproject token and host its init looks up. Those live in the same gitignored env\nfile, and a compiled binary or a bare interpreter loads nothing on its own.\n\nSo open the init point and note the variables it reads. When the project\nalready loads the env file for them, you're done: never remove an env-file\nflag or loader the project's `start` already has, because that local loading\nis the project's choice. When it doesn't, do not wire\na loader into the run script: production supplies these as real environment\nvariables, and a script that loads the gitignored file breaks wherever it is\nmissing. Never put a hard `--env-file=.env` on `start`. Leave the script as it\nis and name the gap in your handoff as a manual follow-up, with the variable\nnames. Leave it out of the handoff and the build is green, the symbols are\nuploaded, and the single command you hand the user to verify with is the one\ncommand that cannot capture.\n\nPut a multi-step build somewhere the tool actually runs it — a script file, a\nmake target, the manifest's own scripts — never an alias mechanism you are\nassuming exists, borrowed from a neighbouring tool.\n\nMatch the bundle's module format to the package's type. A build passes either\nway, and the mismatch only shows when the bundle starts: `esbuild\n--platform=node` emits CommonJS unless you pass `--format=esm`, so in a package\nwhose `package.json` sets `\"type\": \"module\"` the bundle dies at boot with\n`ReferenceError: module is not defined in ES module scope`. Read the `type`\nfield before writing the command.\n\n## The names are a contract\n\nAn env variable only works if the name the code reads is the name in the file.\nYou write the code that reads them; the `credentials` task writes the file, and\nyou two run in parallel. So never invent a name that already exists somewhere\nelse — look for the other half of the contract first, and adopt it:\n\n- **Call `check_env_keys` before you write the config.** It returns names, never\n values. If the PostHog upload variables are already there, make your config\n read exactly those names, whatever they are, even when they are not the ones\n your skill's example shows.\n- **Only when they are absent yet** are you the one deciding. Use the names your\n skill documents for the mechanism you are wiring, and name them in your\n handoff in full so `credentials` and `wire-ci` can match them.\n\nNames that merely look plausible are the failure here. A build reading\n`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no\nerror anywhere — the upload is skipped silently, the build looks clean, and\nevery stack trace stays minified.\n\nDo not write any credential values and do not create env files — the\n`credentials` task owns that, in parallel with you.\n\n## Build it\n\nOnce your edits are in, run the production build and read its exit status, not\nonly its log: the upload often runs before the type check, so the maps can\nland in a build that still fails. Fix what your changes broke and build again.\nA failure that was there before your changes is not yours: note it in your\nhandoff and leave it. Let the build check option names and types: write from\nyour skill, build, and open a plugin's type declarations only to fix an error\nthe build reports.\n\nBuild, never run. A command that installs or launches the app (`expo run:*`,\n`react-native run-*`, `flutter run`, `./gradlew install*`) opens a simulator\nor an emulator on the user's machine, even where your skill names it as the\nbuild. On an Expo app whose `android/` and `ios/` folders are generated, build\nthe JavaScript bundle instead: `npx expo export --source-maps --platform\n`. Your shell blocks `make`, shell scripts, and a Gradle wrapper\noutside the project root: when you put the build in a `make` target or a\nscript, run the build command inside it directly. Never add a package script\nto get past a blocked command. Skip it, and say in your handoff what you could\nnot run.\n\nYour shell gets a reduced environment: `ANDROID_HOME` and other variables from\nthe user's shell do not reach it. A build that cannot find an SDK (`SDK\nlocation not found`) failed because of this shell, not the project. Do not\nretry it, and do not give the user a setup step for it: say the build could\nnot run here.\n\nThe `credentials` task may not have written the upload credentials yet, so this\nbuild may run without them. It has to pass either way. A teammate's checkout\nand a pull-request build have no key either, and a config that demands one\nstops their builds. When the build fails only because the upload credentials\nare missing, make the upload skip itself when they are absent, with the option\nyour skill's plugin or CLI offers, and build again. Once the credentials are\nwritten you cannot build without them: the build reads the env file, and a\nprefix such as `VAR= npm run build` is blocked. Check the skip path by reading\nthe config instead.\n\nWhen the credentials are in place, the build creates a release and uploads to\nthe user's project. That is expected; it is what every production build will\ndo.\n\n## How you know you succeeded\n\nThe build config carries the skill's source-map / debug-symbol changes and can\nread its credentials from the environment at build time. The production build\npasses with your changes, with or without the upload credentials. Your handoff\nnames every file you changed, the exact build-config keys you added, and the\nbuild command you ran with its result and whether the credentials were\npresent, so the CI task can wire the same variables through the pipeline.\n", + credentials: + '---\ntype: credentials\nflow: error-tracking\nlabel: Get and write the upload credentials\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nPut the PostHog source-map upload credentials into this project\'s environment.\nInstall the skill your task input names (`install_skill` with the `skillId`)\nand follow its **"Write credentials to the env file"** step for the variable\nnames and the env file to pick.\n\nThe upload needs a PostHog **personal API key** at build time. Only the user can\nmint one — never call the PostHog API or any tool to create it. Get it and\nwrite it in this one task (the key never survives across tasks):\n\n1. Ask with `wizard_ask`, exactly:\n `{ id: "api-key", prompt: "Uploading source maps needs a PostHog personal API key. Source maps let PostHog show your original code in error stack traces, not minified code.\\n\\nCreate a key with the right permissions here:\\n/source-maps-key\\n\\nThen paste it below.", kind: "text", sensitive: true }`\n You receive `{ secretRef: "secret:..." }` — a vaulted reference, never the raw\n value. If `wizard_ask` is unavailable (non-interactive run), report this task\n with status `not needed` and say in your handoff that the user must create\n the key and set the variables themselves; do not block.\n2. Pick the env file per the skill (reuse the one PostHog\'s SDK already writes\n its `POSTHOG_*` vars to, when there is one). Call `check_env_keys` on it\n first (it returns present/absent, never values — never read the file\n directly).\n3. Settle the variable names before you write them — see "The names are a\n contract" below. Then call `set_env_values`, passing the secretRef as a\n value object, not a literal string — e.g.\n `values: { "POSTHOG_CLI_API_KEY": { secretRef: "" }, "POSTHOG_CLI_PROJECT_ID": "", "POSTHOG_CLI_HOST": "" }`\n with whichever names you settled on. The wizard resolves the ref locally, so\n you never see the key value.\n4. Document the same variable names for other developers: append them to\n `.env.example` (create it if the project has none) with empty or\n placeholder values — never a real value, and never the key itself. The\n example file is committed and is the only `.env*` you may write directly;\n it is how the next developer, and the next wizard run\'s `check_env_keys`,\n learns the project expects these variables. The `init` task may write the\n same file beside you, so read it right before you change it and add your\n lines with `edit`; never `write` over it once it exists.\n\nReplace `` and `` from your project context. Do not touch the build config — the `configure` task owns that.\n\n## The names are a contract\n\nAn env variable only works if the name in the file is the name the code reads.\nYou write the file; the `configure` task writes the code that reads it, and you\ntwo run in parallel. So never invent a name that already exists somewhere else\n— look for the other half of the contract first, and adopt it:\n\n- **Read the build config before you choose.** If it already references PostHog\n env variables, use exactly those names, whatever they are, even when they are\n not the ones your skill would suggest. Reading config is safe — it holds\n names, not secrets.\n- **Only when nothing references them yet** are you the one deciding. Use the\n names your skill documents for the mechanism that was actually wired.\n- **Either way, say which set you chose in your handoff, in full.** That is how\n `configure` and `wire-ci` learn what to match. A handoff that says "the usual\n variables" hands the next task the same guess you just made.\n\nNames that merely look plausible are the failure here. A build reading\n`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no\nerror anywhere — the upload simply never runs, and every stack trace stays\nminified.\n\n## How you know you succeeded\n\nThe env file holds the upload variables (the key as a resolved secret, the\nnon-secret project id and host as literals), written through the wizard tools,\nnever hardcoded in source, and `.env.example` documents the same names with\nplaceholders. Your handoff names the env file and every variable name — never\na value — so the CI task carries the same names into the pipeline.\n', + init: "---\ntype: init\nflow: error-tracking\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, set_env_values]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure PostHog is initialized. If the project already has a working\n`posthog.init(...)` (or the framework's equivalent) with its env keys wired,\nleave it alone and say so in your handoff. If it doesn't, create it following\nyour skill — it owns the how: the framework's init point, the env-var wiring\nthrough the wizard tools, and `.env.example`. The `credentials` task may write\n`.env.example` beside you, so read it right before you change it and add your\nlines with `edit`; never `write` over it once it exists, or the upload\nvariables it documented are lost.\n\nYou exist in this flow because the user asked for error tracking on a repo\nwhose PostHog init is missing or unproven. Initialize the SDK so exceptions can\nflow and stop — no instrumentation, no extras. Don't set up exception capture\neither way; the capture-exceptions task after you owns that.\n\n## Initialize the server the project ships\n\nA directory can hold more than one server: a workspace often keeps a small\nscript beside a nested package with its own manifest (`index.js` next to\n`v2/package.json`). Initialize each server the project builds or deploys, the\none with a `build` or `start` script, a Dockerfile, or a job in the pipeline. A\nscript the project only runs in development, with nothing but a `dev` script,\ngets no client. Name the server and its entry file in your handoff, so the\ntasks after you cover the same one.\n\n## An existing init still needs its variable defined\n\n\"Already initialised\" is a property of the pair, not of the call. An init point\nthat reads `process.env.SOMETHING` only works when `SOMETHING` is defined in the\nenv file the project loads. A repo can carry an init that has never once run:\nthe call is there, the variable it names is nowhere, and the client is built\nfrom an empty string.\n\nSo before you leave an existing init alone, call `check_env_keys` on that env\nfile and look for the exact name the init reads. It returns names, never values.\n\n- **Present** — the pair is complete. Leave it alone and say so.\n- **Absent** — the init is not wired yet, whoever wrote it. Write that variable\n with `set_env_values` under the name the code already reads, and document it\n in `.env.example`. Do not rename the code to match a name you would rather\n have written; the code is the half that already exists.\n\nAn empty key is the quiet failure here. A client constructed from `''` throws\nnothing and logs nothing. The build is clean, the app starts, every capture call\nreturns — and no event ever arrives.\n\n## Make the environment actually reachable\n\nWriting the keys to `.env` is only half the job — something has to load that\nfile at runtime, or the app throws on boot and captures nothing.\n\nMost frameworks do it for you: Next, Nuxt, Astro and SvelteKit auto-load `.env`,\nand Vite auto-loads it for client code. Nothing to do there.\n\nA plain Node backend does not — Express, Fastify, Hono, Koa, a raw `node:http`\nserver — and neither does a bare Rollup or webpack config. When the project\nalready loads `.env` there (a `dotenv` import, an env-file flag in its scripts),\nleave it alone.\n\nWhen nothing loads it, do not wire a loader. Don't install a package for it and\ndon't edit `start` or `dev`. Every way in is a guess about where the app runs: a\nhard `--env-file=.env` stops the app wherever the gitignored file is missing,\n`--env-file-if-exists` needs Node 22.9, and putting the variables in front of\nthe command commits the key. Name it in your handoff as a manual follow-up\ninstead, with the variable names and the user's two options: load `.env` with\n`dotenv`, or set the variables in the app's environment. Say that until then,\nthe guard stops the app at boot in development with\n`POSTHOG_… variable required by PostHog is missing or un-configured`. That loud\nfailure is the guard doing its job.\n\nSome platforms have no environment to read at all, and there the answer is not\na loader. Angular on the stock `@angular/build` builder is the common one:\nnothing populates `process.env`, `import.meta.env`, or the project's own\n`src/environments/.env.ts` with your keys.\n\nThe trap is that each of those *looks* like a mechanism. `import.meta.env` and\nthe `NG_APP_` prefix only exist with `@ngx-env/builder` installed, and a\ngenerated `.env.ts` usually carries one unrelated key such as\n`npm_package_version` and nothing else. Read from either and your key is\n`undefined`: in development the guard throws while the module evaluates and the\napp renders a blank page, and in a production build the guard returns quietly,\nso the app looks fine while PostHog never initialises at all.\n\nSo do not wire a lookup unless you have opened the thing it reads from and seen\nyour key defined there. When nothing populates it, write the real public project\ntoken as a literal in the committed `src/environments/*` files. This is the\nskill's \"no valid environment to read from\" case, and the public token is\npublishable — it ships inside the browser bundle either way.\n\n## Leave the build to later tasks\n\nThe `install` task may still be adding the SDK beside you, so do not build\nhere: the build would fail on a package that is about to arrive. The tasks\nafter you build the project once every edit is in, and fix what does not\ncompile.\n\n## How you know you succeeded\n\nAn init point exists with the PostHog env keys present — whether it already\ndid or you just created it — keys in the env file and confirmed there with\n`check_env_keys`, never hardcoded. On a platform that does not auto-load\n`.env` and has no loader, your handoff names it as a manual follow-up; on a platform with no environment at all, the\ntoken is a literal rather than a lookup into something that never defines it.\nYour handoff names the files involved, how the client is constructed, and how\n`.env` reaches it, so the capture-exceptions task can find the init options\nwithout re-discovering them.\n", + install: + "---\ntype: install\nflow: error-tracking\nlabel: Add the PostHog SDK to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-haiku-4-5\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure the PostHog SDK is in the manifest. If it's already installed,\nleave it alone and say so in your handoff. If it isn't, install it following\nyour skill — it owns the version rules, what counts as an environment failure,\nand the fallback. The package manager comes from the section below, which wins\nover your skill's own rule: the skill looks at this directory only.\n\nYou only exist in this flow because the user asked for error tracking on a\nrepo without PostHog. Install the SDK the init will construct — the browser\nSDK for an app with a browser side, the server SDK for a server-only app — and\nstop: no instrumentation, no extras. A second, server-side library for an app\nthat has both is not yours to add. The `capture-exceptions` task adds it when\nit wires server-side capture, so the manifest never carries a library no code\nimports.\n\n## Pick the package manager first\n\nCall `detect_package_manager`. When it names a manager, use it. When it answers\n`No lockfile found. Default to npm`, do not run npm yet: this directory may be\na package inside a workspace, whose lockfile sits at the repository root. Read\n`../package.json` and `../../package.json` up to the root, and look there for a\nlockfile (`pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `package-lock.json`) or a\n`packageManager` field. Reading them is part of this task even though they sit\nabove this directory; edit nothing there. Use the manager they name, from this\ndirectory. npm is right only when no parent holds either: npm in a pnpm or Yarn\nworkspace fails on `workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) or\nleaves a second lockfile behind.\n\n## Python\n\nIn a Python project, install into the virtualenv the project already keeps in\nits directory. When it has none, add the package to the manifest and stop. Do\nnot create one: it would stay behind in the user's project.\n\n## How you know you succeeded\n\nThe SDK is declared in the manifest at a real version — whether it already\nwas or you just installed it — or your handoff plainly says why the\nenvironment stopped you. Your handoff names the manifest and the package, so\nlater steps import it under the name they will actually get.\n", + 'link-releases': + "---\ntype: link-releases\nflow: error-tracking\nlabel: Link deploys to releases\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, detect_package_manager]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture-exceptions]\n---\n\n## Goal\n\nLink every exception this app sends to the production release it came from.\nThis platform ships its source as is, so there is nothing to upload. Instead the\nproduction deploy resolves a PostHog release, and the app starts with that\nrelease's ID in `POSTHOG_RELEASE_ID`. Install the skill your task input names\n(`install_skill` with the `skillId`) and follow it. It owns the minimum SDK\nversion, the CI step per provider, and how the ID crosses each deploy boundary.\n\nYour task has three parts, in this order:\n\n1. **SDK version.** Make sure the project resolves at least the SDK version\n that reads `POSTHOG_RELEASE_ID`, per the skill's \"Update the SDK\" step. An\n older SDK ignores the variable without an error. Refresh the lock for that\n one package with the project's own tool: call `detect_package_manager`\n first. Do not upgrade across a major version when code from before this run\n already calls the SDK; when the `install` handoff says this run added the\n SDK, raise it straight to the minimum. The production runtime\n decides what can install, not the interpreter on this machine: never lower\n the version below the minimum because a local install fails.\n2. **Resolve the release in the production deploy.** Trace the deploy path by\n reading the project's own files: CI workflows, Dockerfiles, compose files,\n deploy scripts, service and pool configs. Add the resolve step to the job\n that deploys to production, and to no other job.\n3. **Get the ID into the running app.** Pass the ID on at every boundary\n between that step and the process that creates the PostHog client, until\n that process starts with `POSTHOG_RELEASE_ID` in its environment.\n\n## Local development stays as it is\n\nThis is a deploy-time change only. Local runs, tests, pull-request builds, and\npreviews send no release ID, and that is correct. So do not touch run scripts,\ndev servers, test config, `.env`, or `.env.example`. Do not ask for a personal\nAPI key, because nothing local uses one. Never run `posthog-cli` yourself: a\nresolve creates a real release in the user's project.\n\nOnly edit CI and deploy files that already exist. When this project has no\ndeploy path you can open and follow, make no CI changes. Report the task done\nwith the SDK update alone, and put the requirement into your handoff as a\nmanual follow-up.\n\n## The last hop decides it\n\nA release ID that stops one boundary short of the app looks correct in the\ndiff and does nothing. The resolve step succeeds, the deploy is green, and\nevery exception still arrives without a release. Each of these boundaries drops\nthe value unless you pass it on: a `docker build`, a container start, an SSH\nsession, a service manager's environment, PHP-FPM's `clear_env`, and Laravel's\ncached config. So walk the path to its end, and pass the value on the same way\nthe deploy already passes the app's other runtime variables.\n\nIn a workspace package, the deploy path usually starts at the repository root,\nabove this directory (`../.github/workflows`, a root `Dockerfile`): look there\nbefore you conclude there is none. When you cannot open those files, name them\nin your handoff.\n\nWhile you are at the start step, check the other names on it too. The app's\nPostHog init reads a token and a host, and the start step may predate PostHog.\nIf it passes the app's variables by hand and leaves those out, add them by the\nnames the app's source reads, and name any secret the user must create.\n\n## How you know you succeeded\n\nThe project resolves an SDK version that reads `POSTHOG_RELEASE_ID`, or your\nhandoff names the upgrade as a follow-up. The production deploy job resolves\nthe release before it ships, without failing the deploy when PostHog cannot be\nreached or a PostHog secret does not exist yet. The user merges this change\nbefore creating the secrets, so never stop a deploy step on an empty PostHog\nvariable (`${VAR:?…}`, an `exit 1`). The ID reaches the app process as\n`POSTHOG_RELEASE_ID`, and no local, test, or pull-request path changed. Your\nhandoff lists the files you changed, the SDK version before and after, the\ndeploy path hop by hop, and every secret and variable the user must create,\nwith its exact name and where it goes.\n", + report: + "---\ntype: report\nflow: error-tracking\nlabel: Summarise and hand off\nmodel_pi: openai/gpt-5.6-luna\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Glob, Grep, Write]\ndisallowedTools: [enqueue_task, publish_handoff]\ndependsOn: [capture-exceptions, wire-ci, link-releases, verify]\n---\n\n## Goal\n\nTell the user what error tracking now does for them and what they still have\nto do, from the handoffs of every task in the run. `read_handoffs` gives you\neach task's report — the capture mechanism, the files changed, the env\nvariable names, the CI secrets and variables to create, and any deploy path\nthat could not be traced. Do not re-derive any of it from the project.\n\nThis flow keeps its report on disk. Write it once, with `write`, to\n`posthog-error-tracking-report.md` at the top level of this project's\ndirectory: the wizard's closing message sends the user to that file and reads\nits warning block, and `test-setup` appends to it. `publish_handoff` is\nblocked for this task; the file is the report. When any handoff carries a\nmanual follow-up, START\nthe report, right under its H1, with this warning block, one short bullet per\nitem. The wizard shows these bullets again when it exits.\n\n```markdown\n> ⚠️ **Needs your attention**\n> - Load `.env` in the app, or set `POSTHOG_API_KEY` in its environment.\n```\n\nThen, when the run wired source-map upload or release linking, or any\nfollow-up has steps, add a **\"What you still need to do\"** section — numbered,\ncopy-pasteable:\n\n1. Create a personal API key at `/source-maps-key` (skip when the\n credentials handoff says the key is already written and no release linking\n was wired). The link opens the key form with the 'Source map upload' preset\n selected; the preset carries the error tracking write scope that both\n uploads and release linking need.\n2. Add it as the CI secret the wire-ci or link-releases step referenced, named\n exactly as in the pipeline config. When link-releases also reads a project\n ID variable (such as `POSTHOG_PROJECT_ID`), add it with the value\n ``, and say where each goes in the CI provider's settings.\n3. Any other manual follow-up the handoffs carry (an env file nothing loads,\n an untraceable deploy path, provider-side settings, installing a missing\n `posthog-cli`, an SDK upgrade across a major version).\n\nThen cover, briefly and concretely:\n\n- How uncaught errors reach PostHog now — the capture mechanism and the files\n that carry it.\n- If the run also installed and initialized the SDK, say so — the user\n started this command without PostHog and now has it.\n- When source-map upload was wired and neither the credentials nor the wire-ci\n handoff reports a skip: the files changed (paths only), the exact production\n build command, and that every production build now uploads.\n- When the build was wired but the credentials or wire-ci handoff reports a\n skip: the same files and command, and that the build is set up to upload.\n Say that uploads start only once the upload variables are present where the\n build runs.\n- What the `verify` handoff found: the build command it ran and whether it\n passed. When it uploaded, name the release and how many files went up, so\n the user knows the first symbol set is already in PostHog. Anything still\n failing is a follow-up in the warning block. When the build could not run in\n the wizard's shell (an SDK it could not find, a blocked command), the\n follow-up is to run that build command once themselves, not to set anything\n up: their own shell has what the wizard's lacked.\n- When it was skipped: one line saying why — readable stack traces on this\n platform, or that Astro is not supported by the uploader. An outcome, not an\n apology. Say plainly that the build command was left untouched.\n- When release linking was wired: the files changed (paths only), the SDK\n version it now requires, and the deploy path the release ID takes to the\n app. Say that each production deploy resolves a release and starts the app\n with `POSTHOG_RELEASE_ID`, so every exception names the release, version, and\n commit it came from. Say plainly that local runs, tests, and pull-request\n builds are unchanged and send no release ID. When the link-releases handoff\n reports no deploy path it could follow, say what the user's deploy must do\n instead.\n- How to verify: trigger any error and look at\n `/project//error_tracking`; uploaded symbol sets appear\n at `/project//error_tracking/configuration`. With\n release linking, check after the next production deploy that the stack trace\n shows the release, and that the exception's `$release_id` matches the ID the\n deploy resolved.\n\nNever write a secret value into the report — only variable names. Replace\n`` and `` from your project context. Give the same\nsummary in chat, warning block first.\n\n## How you know you succeeded\n\n`posthog-error-tracking-report.md` exists on disk, written in one `write`\ncall, and a user who reads only the file knows\nhow errors reach PostHog, whether the build passed with the changes, the\nfollow-ups they still owe (the API key and the\nCI secrets and variables named exactly, when upload or release linking was\nwired), and where in PostHog to see the first captured exception.\n", + 'setup-error-tracking': + '---\ntype: setup-error-tracking\nflow: error-tracking\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan a PostHog Error Tracking setup and seed the task queue. The end state:\nerrors the app does not catch reach PostHog; where the platform ships minified\nbundles or stripped binaries, production builds upload the source maps or debug\nsymbols that make the stack traces readable; and where the platform runs its\nsource as is (Python, Ruby, PHP), each production deploy links its errors to a\nrelease.\n\nFirst establish three facts from the repo:\n\n**1. Is PostHog already integrated?** Look for `posthog-js` or a server SDK in\nthe dependency manifests, or a `posthog.init(...)` / snippet in the source.\nCheck the project state for existing events if the repo is ambiguous.\n\nIntegrated means the init runs, not that the package is listed. An init point\nis a pair: the call, and the key it is constructed from. A repo can carry the\ncall while the key it names is defined nowhere — then the client is built from\nan empty string and captures nothing, however complete the manifest looks. So\nwhen you find an init that reads a variable, look for that same name in the\nrepo\'s committed env template or its build config. Found, or the project state\nshows real events arriving: integrated. Named nowhere, or you cannot tell:\n**queue `init`**. It re-checks the pair itself and leaves a complete init\nalone, so queuing it when you are unsure costs one cheap task, while skipping\nit on a keyless init costs the whole run.\n\n**2. Which uploader variant is this project — or none?** Read the manifests\nand pick at most one, by this precedence (first match wins):\n\n- `react-native` or `expo` in `package.json` dependencies → `react-native`.\n A React Native repo also carries `ios/Podfile`, an `.xcodeproj` and Gradle\n files for its native shells, so this rule wins over every native marker\n below. Picking `ios` or `android` for it uploads no JavaScript source maps,\n and every JavaScript stack trace stays minified.\n- `pubspec.yaml` → `flutter`\n- an `.xcodeproj`, `Podfile`, or `Package.swift` → `ios`\n- a Gradle build file that applies `com.android.application` or\n `com.android.library`, or an `AndroidManifest.xml` → `android`. A Gradle\n project without these markers is a JVM server: keep reading.\n- `go.mod` → `go`\n- `Cargo.toml` → `rust`\n- `astro` in `package.json` dependencies → **none**. Astro is not supported by\n the uploader: it inlines scripts below its asset limit into the HTML, so a\n build routinely emits a `.map` with no `.js` beside it, and the upload step\n then fails the whole build. This rule wins over every `package.json` match\n below — an Astro project that also depends on `vite` is still **none**.\n- otherwise read `package.json` dependencies, first match wins:\n `nuxt` → `nuxt`; `next` → `nextjs`;\n `@angular/core` → `angular`; `vite` → `vite`; `webpack` → `webpack`;\n `rollup` → `rollup`; `react` → `react`; server-only Node → `node`;\n any other browser JS → `web`. Server-only Node is `node` only when a\n build compiles or bundles it (a `build` script running `tsc`, esbuild, or\n another bundler), in this directory or in a package nested below it.\n Server JavaScript that no build touches runs as written: **none**.\n- **none** for platforms whose stack traces are already readable: plain\n Python (Django, Flask, FastAPI), Ruby, PHP, Elixir, JVM servers, .NET.\n Skip the whole upload subgraph for them — a skipped upload on such a\n platform is an outcome, not a gap. Fact 3 decides what Python, Ruby, and\n PHP get instead.\n\nWhen a variant matched, the uploader skill id is\n`error-tracking-upload-source-maps-`. Pass it to the four upload\ntasks as `inputs: { skillId: "", displayName: "" }`\nso no task re-detects.\n\n**3. Which release-linking variant is this project — or none?** It applies\nwhen the app itself is a server that runs Python, Ruby, or PHP source as is:\n\n- a Python app (`requirements*.txt`, `pyproject.toml`, `Pipfile`, or\n `setup.py`; Django, Flask, FastAPI, and plain Python alike) → `python`\n- a Ruby app (`Gemfile` or a `*.gemspec`; Rails, Sinatra, and plain Ruby\n alike) → `ruby`\n- a PHP app (`composer.json`; Laravel and plain PHP alike) → `php`\n- **none** for every other platform. A manifest that only carries tooling for\n another platform does not count: a `Gemfile` for CocoaPods or fastlane beside\n a mobile app, or a `requirements.txt` for scripts beside a Node service. The\n mobile uploader variants (`react-native`, `flutter`, `ios`, `android`) always\n mean none here. Bundled and compiled platforms get their release from the\n uploader, and Elixir, JVM servers, and .NET have no release-linking skill\n yet.\n\nWhen a variant matched, the skill id is\n`error-tracking-link-releases-`. Pass it to `link-releases` as\n`inputs: { skillId: "", displayName: "" }`. Fact 3\ndoes not depend on fact 2: a Django app that also bundles a Vite frontend in\nthe same project gets both the upload subgraph and `link-releases`, and both\ndefault to the same release.\n\nThe three facts are independent — settle ALL of them before you enqueue\nanything. "PostHog is already integrated" answers fact 1 only; it never decides\nfact 2 or fact 3, and an already-integrated project still gets the upload\nsubgraph or `link-releases` when a variant matches. A compiled or bundled JS\nproject normally has an uploader variant: a Node service built with `tsc` ships\nminified/compiled output, so it is the `node` variant, not "none". Only three\nkinds of project skip the upload subgraph — the readable-stack platforms listed\nabove, Astro, and server JavaScript that no build compiles or bundles.\n\nThen seed the graph:\n\n- `install`, only when the SDK is missing from the manifest.\n- `init`, independent of `install` — whenever fact 1 did not show a complete\n pair. Do not stop on an uninstrumented repo, integrate.\n- `capture-exceptions`, after whichever of `install` and `init` you queued\n (with no dependencies when you queued neither).\n- When an uploader variant matched, add the upload subgraph:\n - `credentials`, no dependencies — it stops to ask the user for a personal\n API key, so keep it a root task: the prompt reaches the user early while\n the code tasks run.\n - `configure`, after `capture-exceptions` — build-config changes; it runs\n after the code edits so the two never fight over the same files.\n - `wire-ci`, after `configure` and `credentials`.\n- When a release-linking variant matched, `link-releases`, after\n `capture-exceptions` — and after `wire-ci` too when you queued the upload\n subgraph, because both edit the deploy pipeline. It changes production\n deploys only, needs nothing from the user during the run, and leaves local\n development as it is.\n- When you queued the upload subgraph, `verify`, after every other task that\n edits code: `capture-exceptions`, `configure`, `credentials`, and\n `link-releases` when you queued it. It builds the project once every edit is\n in, with the upload credentials in place, and fixes what broke. It does not\n wait for `wire-ci`: that task edits only CI files, which a local build never\n reads, so the two run side by side. A project without the upload subgraph\n has no build of its own to check (Python, Ruby, and PHP apps build only in\n their deploy), so it gets no `verify`.\n- `report`, after every other queued task except `test-setup`. It writes the\n handoff once the work is done, so it describes what actually shipped.\n- When you queued the upload subgraph, `test-setup` last, after `report` — it\n offers the user an optional local end-to-end test. The report comes first on\n purpose: a user who walks away still gets it.\n\nA task can name another as a dependency only once that one is queued and has\nan id. So queue the tail one call per turn, never batched: `verify` when you\nqueue it, then `report` naming the `verify` id, then `test-setup` naming the\n`report` id. A `report` queued in the same turn as `verify` cannot wait for it,\nand then reports a build that has not finished.\n\nNever plan an identify, capture, dashboard, or session-replay task — this run\nsets up error tracking, not the full integration. The minimal SDK footprint\nthat `install` and `init` leave behind is enough for exceptions to flow.\n\n## How you know you succeeded\n\nEvery task in the chosen graph is queued with that dependency shape, the four\nupload tasks (when queued) share the same `{ skillId, displayName }` inputs,\n`verify` (when queued) depends on every task that edits code except `wire-ci`,\n`report` depends on every other task except `test-setup` (directly or\ntransitively), `test-setup` (when queued) depends on `report`, and the first\ntask is runnable. `link-releases` (when queued) carries its own\n`{ skillId, displayName }` inputs. Your plan states all three facts explicitly:\nwhether PostHog was integrated — and, when you called it integrated, the name\nof the key you found defined —, which uploader variant matched — or, when you\nqueue no upload tasks, why no variant applies: which readable-stack platform\nthis is, or that Astro is not supported by the uploader —, and which\nrelease-linking variant matched, or that none applies. A plan that never\nmentions fact 2 or fact 3 is an incomplete plan, not a decision. Keep labels\nshort — the action in a few words.\n', + 'test-setup': + '---\ntype: test-setup\nflow: error-tracking\nlabel: Offer an optional local test\nsink: true\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask]\ndisallowedTools: [enqueue_task]\ndependsOn: [report]\n---\n\n## Goal\n\nThe setup is done and the report is written. Offer the user an optional,\none-time, end-to-end check that errors reach PostHog with readable stack traces.\n\nYou run after the report on purpose. A user who stepped away still gets a\nfinished setup and a report, so nothing waits on this test.\n\nAsk before you do anything else, with `wizard_ask`:\n`{ id: "test-affordance", prompt: "Error tracking is set up, and the report is in posthog-error-tracking-report.md. Optional: want me to help you test it locally? I\'ll add a temporary test button (or route) and build the app, so you can run it and confirm errors show up in Error Tracking with readable stack traces. Then I\'ll remove it.", kind: "single", options: [{ label: "Yes, help me test it", value: "yes" }, { label: "No, I\'m done", value: "no" }] }`\n\n- **"no"**, no answer (`__cancelled__`), or `wizard_ask` unavailable: report\n this task done right away, noting the test was offered and not taken. Install\n no skill and read nothing first.\n- **"yes"**: install the skill your task input names (`install_skill` with the\n `skillId`) and follow its **"Test the local setup"** step for the\n platform-appropriate affordance, the `captureException` shape, the\n placement, and the read-before-edit / always-revert rules. Add the\n affordance, then run the production build yourself: it uploads the maps for\n the bundle that carries the affordance.\n When the build fails, revert, tell the user what failed, and stop. When it\n passes, pause with a second `wizard_ask` (id `"test-done"`, a single\n `Continue (revert test code)` option) whose prompt gives the rest as literal\n numbered steps: run the built app, trigger the affordance, then confirm the\n error resolves to real source in Error Tracking. When the platform\'s build\n and run are one step (an Xcode Run, `expo run`), give that step to the user\n instead of building. After the user continues, or when that ask comes back\n unanswered, REVERT every test edit per the skill\'s rules. Never leave the\n affordance in place, even if the user says it didn\'t work.\n\nWhen the user says the test failed, add a short **"Local test"** section at the\nend of `posthog-error-tracking-report.md` with what they saw, and say the same\nin chat. The report task has already run, so this is the only place the\nfailure is written down.\n\n## How you know you succeeded\n\nEither the offer was declined or unanswered and no code changed, or the\naffordance was added, tested, and fully reverted. Your handoff says which, and\na failure the user reported is in the report file.\n', + verify: + "---\ntype: verify\nflow: error-tracking\nlabel: Build and check the setup\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, detect_package_manager]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture-exceptions, configure, credentials, link-releases]\n---\n\n## Goal\n\nEvery edit of this run is in. Prove the setup works by building the project\nthe way it builds for production. `read_handoffs` names the files each task\nchanged, the packages it added, and the env variable names it used.\n\nRead the handoffs first. When the `configure` handoff reports a passing\nproduction build that ran with the upload credentials present, and this run\nhas no `link-releases` task, that build already ran on the finished code: every\nother code task finished before `configure` started. Do not build again. Hand\noff its command, result, and uploader summary lines as your own, and say you\nreused them. Otherwise:\n\n1. Install with the project's package manager, so the build sees the manifest\n and lockfile this run left behind. In a workspace package, that is the\n manager whose lockfile sits at the workspace root above this directory.\n2. Run the production build the project defines in this checkout: its package\n manager's build script, or the platform's compiler (Cargo, Go, Gradle,\n Xcode). You cannot run a Dockerfile or a CI job; when that is the only\n build, say so and finish. When the project defines no build, run its\n typecheck or lint script instead; when it defines none of them, say so and\n finish.\n3. Read the exit status first, then the log. When source-map or debug-symbol\n upload is wired and the credentials are written, the log shows the uploader\n creating a release and uploading files. A green build with no upload lines\n means the upload never ran: compare the variable names the build config\n reads with the names in the env file (`check_env_keys`). When the\n `credentials` handoff reports no key, the build still has to pass, with the\n upload skipped.\n4. Fix what this run's changes broke, in the files the handoffs name, and build\n again. A failure that was there before this run is not yours: say so in your\n handoff and leave it. After a few attempts, stop and hand off what still\n fails.\n\nBuild, never run. A command that installs or launches the app (`expo run:*`,\n`react-native run-*`, `flutter run`, `./gradlew install*`) opens a simulator\nor an emulator on the user's machine. On an Expo app whose `android/` and\n`ios/` folders are generated, build the JavaScript bundle instead: `npx expo\nexport --source-maps --platform `.\n\nWhen the upload is a separate `posthog-cli` command rather than part of the\nbuild, or the build sits in a `make` target or a shell script, you cannot run\nthose: run the build command itself, and say so. Never add a package script to\nget past a blocked command. Your shell gets a reduced\nenvironment: `ANDROID_HOME` and other variables from the user's shell do not\nreach it. A build that cannot find an SDK (`SDK location not found`) failed\nbecause of this shell, not the project: do not retry it, and do not give the\nuser a setup step for it. A missing toolchain, or a local interpreter too old\nfor a version this run pinned, is the environment, not the setup: say so and\nfinish, and never lower a version to make a local install pass.\n\nFinish this task `done` even when the build still fails. Your handoff gives\nthe command, the error, and whether this run caused it, and the report turns\nit into a follow-up. A `failed` verify stops the report, and the user gets no\nreport at all.\n\nA build with the credentials in place creates a release and uploads to the\nuser's project. That is expected: it is what every production build will do,\nand it puts the first symbol set in PostHog right away.\n\n## How you know you succeeded\n\nThe production build passes with this run's changes, and when the upload is\nwired with credentials, its log shows the release and the upload. Your handoff\ngives the command you ran, or that you reused `configure`'s build, its result,\nthe uploader's summary lines (release name and how many files it uploaded),\nevery fix you made with its file, and anything that still fails, so the report\ncan tell the user what was verified. The task ends `done` either way.\n", + 'wire-ci': + "---\ntype: wire-ci\nflow: error-tracking\nlabel: Set up CI for automatic uploads\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [configure, credentials]\n---\n\n## Goal\n\nMake the credentials reach the production build wherever it actually runs, so\nsource maps upload on every deploy — not just on a local build. Install the\nskill your task input names (`install_skill` with the `skillId`) and follow its\n**\"Set up CI for automatic uploads\"** step — it owns tracing where the\nproduction build runs and wiring the credentials through every layer, whatever\nthe CI provider.\n\nThe `configure` and `credentials` handoffs already name the build-config keys\nand the exact environment-variable names in use — carry those same names into\nthe pipeline; do not invent new ones. Trace the deploy path by reading the\nproject's own files (CI workflows, Dockerfiles, deploy scripts) — never invent\nconfig that is not there. You cannot create the CI secret that holds the API\nkey; reference it by name and carry that follow-up, plus any deploy path you\ncould not trace, into your handoff for the report.\n\nA package inside a workspace rarely holds its own pipeline. The CI config and\ndeploy files usually sit at the repository root, above this directory\n(`../.github/workflows`, `../../.gitlab-ci.yml`): look there before you\nconclude there is none, and wire the job that builds this package. When you\ncannot open them, name the file and the job in your handoff.\n\nThe user merges this change before they create the secrets. A step that fails\nwhen a PostHog variable is empty (`${VAR:?…}`, an `exit 1`, a required input)\nbreaks every deploy until they do. Pass the variables through and let the\nupload skip itself when they are empty: the build config's gate does that, and a\n`posthog-cli` step in the pipeline needs a condition on the key. Name the\nsecrets in your handoff instead of enforcing them.\n\n## Two things cross the boundary, not one\n\nCredentials are half of it. The uploader also needs a **release identity** — a\nname and a version — and it derives that from the CI's own git variables or\nfrom a `.git` directory. A container build sees neither: `.git` is almost\nalways in `.dockerignore`, and the CI's variables stop at the `docker build`\ncommand. Nor does a hardcoded release name rescue you; the uploader wants both\nhalves, and stops the build when it has only one.\n\nSo forward the provider's git variables into the build the same way you forward\ncredentials, and declare each one as `ARG` **and** `ENV` — `ARG` alone is not\nvisible to the uploader's environment lookup. Your skill's \"Associate the\nrelease with a git commit\" step lists the variables per provider. Read that\nstep even though it is not the CI step: this boundary is where it applies.\n\nForward them only where a provider actually sets them. A project with no\npipeline — an image built and run by hand — has nothing to inherit from, and\nvariables declared but never filled resolve to no release at all: it reads as\nwired and still fails the build. There the identity has to be supplied outright,\nboth halves, from something the build itself holds — a build argument the\noperator passes, or the manifest's own version — so name and version are always\npresent. Decide which case you are in by reading the repo, not by assuming a\nprovider exists.\n\nThat `ARG`-plus-`ENV` shape is for the git variables and the non-secret settings\nonly. The API key is a secret and keeps whatever secret-carrying mechanism the\nbuild system offers — a build secret mounted for the one step that needs it, a\nmasked variable, a secret file. A secret in `ARG` or `ENV` is recorded in the\nbuild history, and the builder itself will warn you: *do not use ARG or ENV\ninstructions for sensitive data*. Widening the git-variable pattern to cover the\nkey is a downgrade, not consistency.\n\nA build cache between the pipeline and the build, such as Turborepo's, is one\nmore boundary. List the upload credentials among the task's hashed inputs\n(Turborepo's `env`), so a build that has them never replays an entry made\nwithout them, and pass the git variables through without hashing them\n(`passThroughEnv`). Leave the cache on: a hit restores output whose chunks are\nalready uploaded.\n\n## The runtime boundary has names too\n\nUpload credentials are a build-time concern. The app also reads its own\nvariables at **run** time, and the step that starts it is another place a name\nhas to match — a `docker run -e`, a compose file, a systemd unit, a platform's\nenvironment settings. That step was written before PostHog existed here, so the\nname it passes is whatever the project used back then. If `init` settled on a\ndifferent one, the deployed process starts with an undefined key.\n\nNothing fails loudly. In production the init guard returns quietly instead of\nthrowing, so the process boots, serves traffic, and reports nothing — the same\nsilent shape as a mismatched upload variable, one boundary later.\n\nSo read the names the app's own source reads, and make the start step pass\nexactly those. Rename the deploy step's variable, not the code: the code is the\nhalf that already exists. Name any secret the user must create under its new\nname in your handoff.\n\n## How you know you succeeded\n\nThe pipeline that runs the production build carries the upload credentials by\nthe same names the credentials task used and reaches the uploader with a\nresolvable release identity, and the step that starts the app passes the\nvariable names the app's own source reads. Every secret the user still has to create is named\nin your handoff. Your handoff lists the CI files you changed\nand every manual follow-up, so the report can hand them to the user.\n", +}; diff --git a/src/programs/error-tracking/agent/capture-exceptions.md b/src/programs/error-tracking/agent/capture-exceptions.md new file mode 100644 index 000000000..2bfd887d9 --- /dev/null +++ b/src/programs/error-tracking/agent/capture-exceptions.md @@ -0,0 +1,116 @@ +--- +type: capture-exceptions +flow: error-tracking +label: Wire up exception capture +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-error-tracking-step, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Make the errors the app does not catch reach PostHog, by whatever means the +SDK offers for that. Which means depends on the SDK: some autocapture +exceptions once you enable it at init, some wire into the framework's own +error handler, some give you a boundary to mount at the app entry. Follow the +docs and the reference example for this one, and set it up once per runtime — +never manual capture calls sprinkled across files. + +An app that runs code on a server as well as in the browser has two runtimes, +and each needs its own capture. Cover every runtime the docs cover for this +framework and the app actually uses: the server side takes the SDK's server +library, where the framework hands you its errors (for Next.js, +`onRequestError` in `instrumentation.ts`). A browser-only setup leaves every +server error uncaptured. An Electron app has the same split: its main process +runs Node, and its uncaught errors never reach the renderer's capture, so the +main process takes the server library. A packaged app ships without the +developer's `.env`, so a runtime `process.env` lookup in the main process is +always empty there: read the token the way the bundler inlines it at build time +(electron-vite: `import.meta.env.MAIN_VITE_*`). A fully static app has no +server runtime to cover. + +A page often shows two setups side by side: a global initializer, and an +explicit client you hold. Follow one of them the whole way through. Taking the +wiring from one and the calls from the other reads fine and compiles nowhere — +an initializer that hands back nothing leaves you nothing to call methods on. + +The SDK is installed and initialized — either it already was, or the install +and init tasks before you did it (see their handoffs); build on that, do not +re-check it. + +Install nothing except the server library your server-side capture imports, +when the manifest lacks it: add it with the project's package manager +(`detect_package_manager`). When it answers `No lockfile found. Default to +npm`, look up the tree first: in a workspace package, use the manager whose +lockfile sits at the workspace root above this directory, and npm only when no +parent holds a lockfile. Do not run tests or start the app, and do not touch +the build config: when the flow includes a configure task, it owns those files. +Stay inside this project's directory and set up capture in those places; that +is the whole job. + +Each import needs its declaration in the manifest of the package that holds the +importing file. An app inside a workspace often nests its own package (a +`v2/package.json` below this directory): when the SDK sits in a different +manifest than the code that imports it, add it to the importing package too, +with the workspace's manager. Otherwise the import resolves only through +hoisting and breaks in a focused install or a container build. + +Once your edits are in, run the project's build, or its typecheck when that +covers your files and is quicker, and fix what your edits broke. A failure +that was there before your edits is not yours: note it in your handoff and +leave it. Only a build the project defines in this checkout counts: a package +script or a compiler. Most Python, Ruby, and PHP apps have none, and then +there is nothing to run: do not create environments, install packages, or +reach for Docker to check your edit. Let the build check API names and types: write from the docs and +build, and open an SDK's type declarations only to fix an error the build +reports. + +Build, never run: no command that installs or launches the app (`expo run:*`, +`react-native run-*`, `flutter run`), because it opens a simulator or an +emulator on the user's machine. Leave no build output the project does not +ignore: in Go, check with `go vet ./...`, because `go build ./...` writes a +main package's binary into the project root. Your shell gets a reduced +environment, so a build that cannot find an SDK (`SDK location not found`) +failed because of this shell, not your edit: do not retry it, and say so in +your handoff. + +## Do not copy the SDK's own listeners + +Exception autocapture already registers the global handlers: `window.onerror` +and `unhandledrejection` in the browser, `uncaughtException` and +`unhandledRejection` in Node, `sys.excepthook` in Python, the panic hook in +Rust, and the uncaught-exception and signal handlers on iOS and Android. Turn +that on with the SDK's own option — `capture_exceptions`, +`enableExceptionAutocapture`, `enable_exception_autocapture`, `capture_panics`, +`errorTrackingConfig.autoCapture` — and register none of those handlers +yourself. A second listener on the same event sends every error twice, and it +stops matching the SDK's handling the moment the SDK changes. + +What the SDK cannot see is yours to add: errors a framework catches before any +global handler fires. Express error middleware, Fastify `setErrorHandler`, Hono +`onError`, Vue `app.config.errorHandler`, Angular `ErrorHandler`, SvelteKit +`handleError`, a React error boundary, Next.js `global-error` — hook those, +because the framework swallows the error and the global listener never hears +of it. NestJS is one more layer up: its exception filters answer every request +error before Express error middleware runs, so capture goes in a global filter +(the app's existing `@Catch()` filter when it has one), not in +`setupExpressErrorHandler`. A platform with no autocapture at all, such as Go, +is the other case: there the capture boundary at the entry point is the +mechanism, not a copy of one. + +## How you know you succeeded + +An error the app does not catch reaches PostHog from every runtime the app +runs in, through the mechanism each SDK gives you rather than one you invented, +and every PostHog library in the manifest is imported. No global error listener +of your own sits beside the SDK's autocapture. The project builds as well as it +did before your edits. You installed nothing but the server library your +capture imports, and did not run tests, search outside the project, or read +through the whole app or hand-wrap individual components or routes. Your handoff names the files you changed, the capture mechanism, and +the build command you ran with its result, so the report can explain it to the +user. diff --git a/src/programs/error-tracking/agent/configure.md b/src/programs/error-tracking/agent/configure.md new file mode 100644 index 000000000..3b9a49f11 --- /dev/null +++ b/src/programs/error-tracking/agent/configure.md @@ -0,0 +1,173 @@ +--- +type: configure +flow: error-tracking +label: Apply build-config changes +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, check_env_keys] +disallowedTools: [enqueue_task] +dependsOn: [capture-exceptions] +--- + +## Goal + +Make this project's production build emit and upload source maps or debug +symbols. Flutter Android/iOS release builds upload Dart AOT symbols separately +from platform-native symbols. Install the skill your task input names +(`install_skill` with the `skillId`) and read it — it is the source of truth +for the per-framework build-config and the uploader wiring. + +Two of the skill's steps are yours: + +- **"Apply build-config changes"** — make the bundler / build-config edits the + skill instructs for this platform, so the build produces matching maps or + debug symbols and runs the uploader. Only web source maps need chunk IDs. +- **"Make credentials available at build time"** — do the skill's step so the + build can read the upload credentials from the environment. If it calls for a + loader (e.g. `dotenv`), install it SILENTLY with the project's package + manager. Skip this step entirely when the platform already auto-loads `.env`. + +Install every dependency with the project's own package manager: call +`detect_package_manager` before the first install and use its answer, +translating any `npm install` the skill or docs show (`pnpm add -D …`, +`yarn add -D …`). In a pnpm or yarn workspace, npm fails outright on +`workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) — that error means the +wrong manager, never a flag to retry with. When `detect_package_manager` +answers `No lockfile found. Default to npm`, look up the tree first: in a +workspace package, use the manager whose lockfile sits at the workspace root +above this directory, and npm only when no parent holds a lockfile. + +Change only what the upload needs. Do not remove or weaken a source-map setting +the project already has (`productionBrowserSourceMaps`, Vite's +`build.sourcemap`, webpack's `devtool`): a build without the upload credentials +must still emit what it emitted before. Do not upgrade a package the project +already depends on to quiet a peer-dependency warning, because a warning is not +a failure. Upgrade one only when the install or build fails on it, or when the +skill requires a newer version for the feature you are wiring. Keep the +project's version style: an exact pin stays exact. + +Wire the upload into a build the project already has, in this directory or in +a package nested below it. Code that no build compiles or bundles runs as +written, and its stack traces are already readable: do not add a bundler, a +compiler, or a new `build` or `start` script to create one. When this project +has no production build to attach an upload to, report this task as +`not needed` and say why in your handoff. + +When the upload step you wire runs a machine-global `posthog-cli` rather than +one from the project's own dependencies, do not try to install it or check for +it: a global install, `posthog-cli` itself, and `command -v` are blocked for +you, and the wizard already tried the install before your task. Name it in +your handoff as a follow-up for every other machine that runs the release +build — `npm install -g @posthog/cli@latest` — because without it the upload +step cannot run. + +When your changes make the build emit a bundle to a new directory (`dist/`, +`build/`), check that some script actually runs that output. A project whose +`build` writes `dist/index.js` while `start` still runs the original source +never executes the bundle the maps were uploaded for, so every uploaded map +goes unused. Add or fix the script that serves the built output. + +That run script has its own environment to satisfy, and it is not the build's. +The uploader's credentials are handled — your build step passes them itself. The +separate question is the variables **the application reads when it starts**: the +project token and host its init looks up. Those live in the same gitignored env +file, and a compiled binary or a bare interpreter loads nothing on its own. + +So open the init point and note the variables it reads. When the project +already loads the env file for them, you're done: never remove an env-file +flag or loader the project's `start` already has, because that local loading +is the project's choice. When it doesn't, do not wire +a loader into the run script: production supplies these as real environment +variables, and a script that loads the gitignored file breaks wherever it is +missing. Never put a hard `--env-file=.env` on `start`. Leave the script as it +is and name the gap in your handoff as a manual follow-up, with the variable +names. Leave it out of the handoff and the build is green, the symbols are +uploaded, and the single command you hand the user to verify with is the one +command that cannot capture. + +Put a multi-step build somewhere the tool actually runs it — a script file, a +make target, the manifest's own scripts — never an alias mechanism you are +assuming exists, borrowed from a neighbouring tool. + +Match the bundle's module format to the package's type. A build passes either +way, and the mismatch only shows when the bundle starts: `esbuild +--platform=node` emits CommonJS unless you pass `--format=esm`, so in a package +whose `package.json` sets `"type": "module"` the bundle dies at boot with +`ReferenceError: module is not defined in ES module scope`. Read the `type` +field before writing the command. + +## The names are a contract + +An env variable only works if the name the code reads is the name in the file. +You write the code that reads them; the `credentials` task writes the file, and +you two run in parallel. So never invent a name that already exists somewhere +else — look for the other half of the contract first, and adopt it: + +- **Call `check_env_keys` before you write the config.** It returns names, never + values. If the PostHog upload variables are already there, make your config + read exactly those names, whatever they are, even when they are not the ones + your skill's example shows. +- **Only when they are absent yet** are you the one deciding. Use the names your + skill documents for the mechanism you are wiring, and name them in your + handoff in full so `credentials` and `wire-ci` can match them. + +Names that merely look plausible are the failure here. A build reading +`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no +error anywhere — the upload is skipped silently, the build looks clean, and +every stack trace stays minified. + +Do not write any credential values and do not create env files — the +`credentials` task owns that, in parallel with you. + +## Build it + +Once your edits are in, run the production build and read its exit status, not +only its log: the upload often runs before the type check, so the maps can +land in a build that still fails. Fix what your changes broke and build again. +A failure that was there before your changes is not yours: note it in your +handoff and leave it. Let the build check option names and types: write from +your skill, build, and open a plugin's type declarations only to fix an error +the build reports. + +Build, never run. A command that installs or launches the app (`expo run:*`, +`react-native run-*`, `flutter run`, `./gradlew install*`) opens a simulator +or an emulator on the user's machine, even where your skill names it as the +build. On an Expo app whose `android/` and `ios/` folders are generated, build +the JavaScript bundle instead: `npx expo export --source-maps --platform +`. Your shell blocks `make`, shell scripts, and a Gradle wrapper +outside the project root: when you put the build in a `make` target or a +script, run the build command inside it directly. Never add a package script +to get past a blocked command. Skip it, and say in your handoff what you could +not run. + +Your shell gets a reduced environment: `ANDROID_HOME` and other variables from +the user's shell do not reach it. A build that cannot find an SDK (`SDK +location not found`) failed because of this shell, not the project. Do not +retry it, and do not give the user a setup step for it: say the build could +not run here. + +The `credentials` task may not have written the upload credentials yet, so this +build may run without them. It has to pass either way. A teammate's checkout +and a pull-request build have no key either, and a config that demands one +stops their builds. When the build fails only because the upload credentials +are missing, make the upload skip itself when they are absent, with the option +your skill's plugin or CLI offers, and build again. Once the credentials are +written you cannot build without them: the build reads the env file, and a +prefix such as `VAR= npm run build` is blocked. Check the skip path by reading +the config instead. + +When the credentials are in place, the build creates a release and uploads to +the user's project. That is expected; it is what every production build will +do. + +## How you know you succeeded + +The build config carries the skill's source-map / debug-symbol changes and can +read its credentials from the environment at build time. The production build +passes with your changes, with or without the upload credentials. Your handoff +names every file you changed, the exact build-config keys you added, and the +build command you ran with its result and whether the credentials were +present, so the CI task can wire the same variables through the pipeline. diff --git a/src/programs/error-tracking/agent/credentials.md b/src/programs/error-tracking/agent/credentials.md new file mode 100644 index 000000000..d748955b9 --- /dev/null +++ b/src/programs/error-tracking/agent/credentials.md @@ -0,0 +1,81 @@ +--- +type: credentials +flow: error-tracking +label: Get and write the upload credentials +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Put the PostHog source-map upload credentials into this project's environment. +Install the skill your task input names (`install_skill` with the `skillId`) +and follow its **"Write credentials to the env file"** step for the variable +names and the env file to pick. + +The upload needs a PostHog **personal API key** at build time. Only the user can +mint one — never call the PostHog API or any tool to create it. Get it and +write it in this one task (the key never survives across tasks): + +1. Ask with `wizard_ask`, exactly: + `{ id: "api-key", prompt: "Uploading source maps needs a PostHog personal API key. Source maps let PostHog show your original code in error stack traces, not minified code.\n\nCreate a key with the right permissions here:\n/source-maps-key\n\nThen paste it below.", kind: "text", sensitive: true }` + You receive `{ secretRef: "secret:..." }` — a vaulted reference, never the raw + value. If `wizard_ask` is unavailable (non-interactive run), report this task + with status `not needed` and say in your handoff that the user must create + the key and set the variables themselves; do not block. +2. Pick the env file per the skill (reuse the one PostHog's SDK already writes + its `POSTHOG_*` vars to, when there is one). Call `check_env_keys` on it + first (it returns present/absent, never values — never read the file + directly). +3. Settle the variable names before you write them — see "The names are a + contract" below. Then call `set_env_values`, passing the secretRef as a + value object, not a literal string — e.g. + `values: { "POSTHOG_CLI_API_KEY": { secretRef: "" }, "POSTHOG_CLI_PROJECT_ID": "", "POSTHOG_CLI_HOST": "" }` + with whichever names you settled on. The wizard resolves the ref locally, so + you never see the key value. +4. Document the same variable names for other developers: append them to + `.env.example` (create it if the project has none) with empty or + placeholder values — never a real value, and never the key itself. The + example file is committed and is the only `.env*` you may write directly; + it is how the next developer, and the next wizard run's `check_env_keys`, + learns the project expects these variables. The `init` task may write the + same file beside you, so read it right before you change it and add your + lines with `edit`; never `write` over it once it exists. + +Replace `` and `` from your project context. Do not touch the build config — the `configure` task owns that. + +## The names are a contract + +An env variable only works if the name in the file is the name the code reads. +You write the file; the `configure` task writes the code that reads it, and you +two run in parallel. So never invent a name that already exists somewhere else +— look for the other half of the contract first, and adopt it: + +- **Read the build config before you choose.** If it already references PostHog + env variables, use exactly those names, whatever they are, even when they are + not the ones your skill would suggest. Reading config is safe — it holds + names, not secrets. +- **Only when nothing references them yet** are you the one deciding. Use the + names your skill documents for the mechanism that was actually wired. +- **Either way, say which set you chose in your handoff, in full.** That is how + `configure` and `wire-ci` learn what to match. A handoff that says "the usual + variables" hands the next task the same guess you just made. + +Names that merely look plausible are the failure here. A build reading +`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no +error anywhere — the upload simply never runs, and every stack trace stays +minified. + +## How you know you succeeded + +The env file holds the upload variables (the key as a resolved secret, the +non-secret project id and host as literals), written through the wizard tools, +never hardcoded in source, and `.env.example` documents the same names with +placeholders. Your handoff names the env file and every variable name — never +a value — so the CI task carries the same names into the pipeline. diff --git a/src/programs/error-tracking/agent/init.md b/src/programs/error-tracking/agent/init.md new file mode 100644 index 000000000..b1928af4a --- /dev/null +++ b/src/programs/error-tracking/agent/init.md @@ -0,0 +1,121 @@ +--- +type: init +flow: error-tracking +label: Set up PostHog initialization +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-init, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, set_env_values] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure PostHog is initialized. If the project already has a working +`posthog.init(...)` (or the framework's equivalent) with its env keys wired, +leave it alone and say so in your handoff. If it doesn't, create it following +your skill — it owns the how: the framework's init point, the env-var wiring +through the wizard tools, and `.env.example`. The `credentials` task may write +`.env.example` beside you, so read it right before you change it and add your +lines with `edit`; never `write` over it once it exists, or the upload +variables it documented are lost. + +You exist in this flow because the user asked for error tracking on a repo +whose PostHog init is missing or unproven. Initialize the SDK so exceptions can +flow and stop — no instrumentation, no extras. Don't set up exception capture +either way; the capture-exceptions task after you owns that. + +## Initialize the server the project ships + +A directory can hold more than one server: a workspace often keeps a small +script beside a nested package with its own manifest (`index.js` next to +`v2/package.json`). Initialize each server the project builds or deploys, the +one with a `build` or `start` script, a Dockerfile, or a job in the pipeline. A +script the project only runs in development, with nothing but a `dev` script, +gets no client. Name the server and its entry file in your handoff, so the +tasks after you cover the same one. + +## An existing init still needs its variable defined + +"Already initialised" is a property of the pair, not of the call. An init point +that reads `process.env.SOMETHING` only works when `SOMETHING` is defined in the +env file the project loads. A repo can carry an init that has never once run: +the call is there, the variable it names is nowhere, and the client is built +from an empty string. + +So before you leave an existing init alone, call `check_env_keys` on that env +file and look for the exact name the init reads. It returns names, never values. + +- **Present** — the pair is complete. Leave it alone and say so. +- **Absent** — the init is not wired yet, whoever wrote it. Write that variable + with `set_env_values` under the name the code already reads, and document it + in `.env.example`. Do not rename the code to match a name you would rather + have written; the code is the half that already exists. + +An empty key is the quiet failure here. A client constructed from `''` throws +nothing and logs nothing. The build is clean, the app starts, every capture call +returns — and no event ever arrives. + +## Make the environment actually reachable + +Writing the keys to `.env` is only half the job — something has to load that +file at runtime, or the app throws on boot and captures nothing. + +Most frameworks do it for you: Next, Nuxt, Astro and SvelteKit auto-load `.env`, +and Vite auto-loads it for client code. Nothing to do there. + +A plain Node backend does not — Express, Fastify, Hono, Koa, a raw `node:http` +server — and neither does a bare Rollup or webpack config. When the project +already loads `.env` there (a `dotenv` import, an env-file flag in its scripts), +leave it alone. + +When nothing loads it, do not wire a loader. Don't install a package for it and +don't edit `start` or `dev`. Every way in is a guess about where the app runs: a +hard `--env-file=.env` stops the app wherever the gitignored file is missing, +`--env-file-if-exists` needs Node 22.9, and putting the variables in front of +the command commits the key. Name it in your handoff as a manual follow-up +instead, with the variable names and the user's two options: load `.env` with +`dotenv`, or set the variables in the app's environment. Say that until then, +the guard stops the app at boot in development with +`POSTHOG_… variable required by PostHog is missing or un-configured`. That loud +failure is the guard doing its job. + +Some platforms have no environment to read at all, and there the answer is not +a loader. Angular on the stock `@angular/build` builder is the common one: +nothing populates `process.env`, `import.meta.env`, or the project's own +`src/environments/.env.ts` with your keys. + +The trap is that each of those *looks* like a mechanism. `import.meta.env` and +the `NG_APP_` prefix only exist with `@ngx-env/builder` installed, and a +generated `.env.ts` usually carries one unrelated key such as +`npm_package_version` and nothing else. Read from either and your key is +`undefined`: in development the guard throws while the module evaluates and the +app renders a blank page, and in a production build the guard returns quietly, +so the app looks fine while PostHog never initialises at all. + +So do not wire a lookup unless you have opened the thing it reads from and seen +your key defined there. When nothing populates it, write the real public project +token as a literal in the committed `src/environments/*` files. This is the +skill's "no valid environment to read from" case, and the public token is +publishable — it ships inside the browser bundle either way. + +## Leave the build to later tasks + +The `install` task may still be adding the SDK beside you, so do not build +here: the build would fail on a package that is about to arrive. The tasks +after you build the project once every edit is in, and fix what does not +compile. + +## How you know you succeeded + +An init point exists with the PostHog env keys present — whether it already +did or you just created it — keys in the env file and confirmed there with +`check_env_keys`, never hardcoded. On a platform that does not auto-load +`.env` and has no loader, your handoff names it as a manual follow-up; on a platform with no environment at all, the +token is a literal rather than a lookup into something that never defines it. +Your handoff names the files involved, how the client is constructed, and how +`.env` reaches it, so the capture-exceptions task can find the init options +without re-discovering them. diff --git a/src/programs/error-tracking/agent/install.md b/src/programs/error-tracking/agent/install.md new file mode 100644 index 000000000..0bb1cb16b --- /dev/null +++ b/src/programs/error-tracking/agent/install.md @@ -0,0 +1,54 @@ +--- +type: install +flow: error-tracking +label: Add the PostHog SDK to the manifest +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-haiku-4-5 +skills: [integration-v2-install] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure the PostHog SDK is in the manifest. If it's already installed, +leave it alone and say so in your handoff. If it isn't, install it following +your skill — it owns the version rules, what counts as an environment failure, +and the fallback. The package manager comes from the section below, which wins +over your skill's own rule: the skill looks at this directory only. + +You only exist in this flow because the user asked for error tracking on a +repo without PostHog. Install the SDK the init will construct — the browser +SDK for an app with a browser side, the server SDK for a server-only app — and +stop: no instrumentation, no extras. A second, server-side library for an app +that has both is not yours to add. The `capture-exceptions` task adds it when +it wires server-side capture, so the manifest never carries a library no code +imports. + +## Pick the package manager first + +Call `detect_package_manager`. When it names a manager, use it. When it answers +`No lockfile found. Default to npm`, do not run npm yet: this directory may be +a package inside a workspace, whose lockfile sits at the repository root. Read +`../package.json` and `../../package.json` up to the root, and look there for a +lockfile (`pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `package-lock.json`) or a +`packageManager` field. Reading them is part of this task even though they sit +above this directory; edit nothing there. Use the manager they name, from this +directory. npm is right only when no parent holds either: npm in a pnpm or Yarn +workspace fails on `workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) or +leaves a second lockfile behind. + +## Python + +In a Python project, install into the virtualenv the project already keeps in +its directory. When it has none, add the package to the manifest and stop. Do +not create one: it would stay behind in the user's project. + +## How you know you succeeded + +The SDK is declared in the manifest at a real version — whether it already +was or you just installed it — or your handoff plainly says why the +environment stopped you. Your handoff names the manifest and the package, so +later steps import it under the name they will actually get. diff --git a/src/programs/error-tracking/agent/link-releases.md b/src/programs/error-tracking/agent/link-releases.md new file mode 100644 index 000000000..2e9645010 --- /dev/null +++ b/src/programs/error-tracking/agent/link-releases.md @@ -0,0 +1,87 @@ +--- +type: link-releases +flow: error-tracking +label: Link deploys to releases +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, detect_package_manager] +disallowedTools: [enqueue_task] +dependsOn: [capture-exceptions] +--- + +## Goal + +Link every exception this app sends to the production release it came from. +This platform ships its source as is, so there is nothing to upload. Instead the +production deploy resolves a PostHog release, and the app starts with that +release's ID in `POSTHOG_RELEASE_ID`. Install the skill your task input names +(`install_skill` with the `skillId`) and follow it. It owns the minimum SDK +version, the CI step per provider, and how the ID crosses each deploy boundary. + +Your task has three parts, in this order: + +1. **SDK version.** Make sure the project resolves at least the SDK version + that reads `POSTHOG_RELEASE_ID`, per the skill's "Update the SDK" step. An + older SDK ignores the variable without an error. Refresh the lock for that + one package with the project's own tool: call `detect_package_manager` + first. Do not upgrade across a major version when code from before this run + already calls the SDK; when the `install` handoff says this run added the + SDK, raise it straight to the minimum. The production runtime + decides what can install, not the interpreter on this machine: never lower + the version below the minimum because a local install fails. +2. **Resolve the release in the production deploy.** Trace the deploy path by + reading the project's own files: CI workflows, Dockerfiles, compose files, + deploy scripts, service and pool configs. Add the resolve step to the job + that deploys to production, and to no other job. +3. **Get the ID into the running app.** Pass the ID on at every boundary + between that step and the process that creates the PostHog client, until + that process starts with `POSTHOG_RELEASE_ID` in its environment. + +## Local development stays as it is + +This is a deploy-time change only. Local runs, tests, pull-request builds, and +previews send no release ID, and that is correct. So do not touch run scripts, +dev servers, test config, `.env`, or `.env.example`. Do not ask for a personal +API key, because nothing local uses one. Never run `posthog-cli` yourself: a +resolve creates a real release in the user's project. + +Only edit CI and deploy files that already exist. When this project has no +deploy path you can open and follow, make no CI changes. Report the task done +with the SDK update alone, and put the requirement into your handoff as a +manual follow-up. + +## The last hop decides it + +A release ID that stops one boundary short of the app looks correct in the +diff and does nothing. The resolve step succeeds, the deploy is green, and +every exception still arrives without a release. Each of these boundaries drops +the value unless you pass it on: a `docker build`, a container start, an SSH +session, a service manager's environment, PHP-FPM's `clear_env`, and Laravel's +cached config. So walk the path to its end, and pass the value on the same way +the deploy already passes the app's other runtime variables. + +In a workspace package, the deploy path usually starts at the repository root, +above this directory (`../.github/workflows`, a root `Dockerfile`): look there +before you conclude there is none. When you cannot open those files, name them +in your handoff. + +While you are at the start step, check the other names on it too. The app's +PostHog init reads a token and a host, and the start step may predate PostHog. +If it passes the app's variables by hand and leaves those out, add them by the +names the app's source reads, and name any secret the user must create. + +## How you know you succeeded + +The project resolves an SDK version that reads `POSTHOG_RELEASE_ID`, or your +handoff names the upgrade as a follow-up. The production deploy job resolves +the release before it ships, without failing the deploy when PostHog cannot be +reached or a PostHog secret does not exist yet. The user merges this change +before creating the secrets, so never stop a deploy step on an empty PostHog +variable (`${VAR:?…}`, an `exit 1`). The ID reaches the app process as +`POSTHOG_RELEASE_ID`, and no local, test, or pull-request path changed. Your +handoff lists the files you changed, the SDK version before and after, the +deploy path hop by hop, and every secret and variable the user must create, +with its exact name and where it goes. diff --git a/src/programs/error-tracking/agent/report.md b/src/programs/error-tracking/agent/report.md new file mode 100644 index 000000000..41e0facff --- /dev/null +++ b/src/programs/error-tracking/agent/report.md @@ -0,0 +1,103 @@ +--- +type: report +flow: error-tracking +label: Summarise and hand off +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Glob, Grep, Write] +disallowedTools: [enqueue_task, publish_handoff] +dependsOn: [capture-exceptions, wire-ci, link-releases, verify] +--- + +## Goal + +Tell the user what error tracking now does for them and what they still have +to do, from the handoffs of every task in the run. `read_handoffs` gives you +each task's report — the capture mechanism, the files changed, the env +variable names, the CI secrets and variables to create, and any deploy path +that could not be traced. Do not re-derive any of it from the project. + +This flow keeps its report on disk. Write it once, with `write`, to +`posthog-error-tracking-report.md` at the top level of this project's +directory: the wizard's closing message sends the user to that file and reads +its warning block, and `test-setup` appends to it. `publish_handoff` is +blocked for this task; the file is the report. When any handoff carries a +manual follow-up, START +the report, right under its H1, with this warning block, one short bullet per +item. The wizard shows these bullets again when it exits. + +```markdown +> ⚠️ **Needs your attention** +> - Load `.env` in the app, or set `POSTHOG_API_KEY` in its environment. +``` + +Then, when the run wired source-map upload or release linking, or any +follow-up has steps, add a **"What you still need to do"** section — numbered, +copy-pasteable: + +1. Create a personal API key at `/source-maps-key` (skip when the + credentials handoff says the key is already written and no release linking + was wired). The link opens the key form with the 'Source map upload' preset + selected; the preset carries the error tracking write scope that both + uploads and release linking need. +2. Add it as the CI secret the wire-ci or link-releases step referenced, named + exactly as in the pipeline config. When link-releases also reads a project + ID variable (such as `POSTHOG_PROJECT_ID`), add it with the value + ``, and say where each goes in the CI provider's settings. +3. Any other manual follow-up the handoffs carry (an env file nothing loads, + an untraceable deploy path, provider-side settings, installing a missing + `posthog-cli`, an SDK upgrade across a major version). + +Then cover, briefly and concretely: + +- How uncaught errors reach PostHog now — the capture mechanism and the files + that carry it. +- If the run also installed and initialized the SDK, say so — the user + started this command without PostHog and now has it. +- When source-map upload was wired and neither the credentials nor the wire-ci + handoff reports a skip: the files changed (paths only), the exact production + build command, and that every production build now uploads. +- When the build was wired but the credentials or wire-ci handoff reports a + skip: the same files and command, and that the build is set up to upload. + Say that uploads start only once the upload variables are present where the + build runs. +- What the `verify` handoff found: the build command it ran and whether it + passed. When it uploaded, name the release and how many files went up, so + the user knows the first symbol set is already in PostHog. Anything still + failing is a follow-up in the warning block. When the build could not run in + the wizard's shell (an SDK it could not find, a blocked command), the + follow-up is to run that build command once themselves, not to set anything + up: their own shell has what the wizard's lacked. +- When it was skipped: one line saying why — readable stack traces on this + platform, or that Astro is not supported by the uploader. An outcome, not an + apology. Say plainly that the build command was left untouched. +- When release linking was wired: the files changed (paths only), the SDK + version it now requires, and the deploy path the release ID takes to the + app. Say that each production deploy resolves a release and starts the app + with `POSTHOG_RELEASE_ID`, so every exception names the release, version, and + commit it came from. Say plainly that local runs, tests, and pull-request + builds are unchanged and send no release ID. When the link-releases handoff + reports no deploy path it could follow, say what the user's deploy must do + instead. +- How to verify: trigger any error and look at + `/project//error_tracking`; uploaded symbol sets appear + at `/project//error_tracking/configuration`. With + release linking, check after the next production deploy that the stack trace + shows the release, and that the exception's `$release_id` matches the ID the + deploy resolved. + +Never write a secret value into the report — only variable names. Replace +`` and `` from your project context. Give the same +summary in chat, warning block first. + +## How you know you succeeded + +`posthog-error-tracking-report.md` exists on disk, written in one `write` +call, and a user who reads only the file knows +how errors reach PostHog, whether the build passed with the changes, the +follow-ups they still owe (the API key and the +CI secrets and variables named exactly, when upload or release linking was +wired), and where in PostHog to see the first captured exception. diff --git a/src/programs/error-tracking/agent/setup-error-tracking.md b/src/programs/error-tracking/agent/setup-error-tracking.md new file mode 100644 index 000000000..67ec23a78 --- /dev/null +++ b/src/programs/error-tracking/agent/setup-error-tracking.md @@ -0,0 +1,170 @@ +--- +type: setup-error-tracking +flow: error-tracking +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, complete_task] +dependsOn: [] +--- + +## Goal + +Plan a PostHog Error Tracking setup and seed the task queue. The end state: +errors the app does not catch reach PostHog; where the platform ships minified +bundles or stripped binaries, production builds upload the source maps or debug +symbols that make the stack traces readable; and where the platform runs its +source as is (Python, Ruby, PHP), each production deploy links its errors to a +release. + +First establish three facts from the repo: + +**1. Is PostHog already integrated?** Look for `posthog-js` or a server SDK in +the dependency manifests, or a `posthog.init(...)` / snippet in the source. +Check the project state for existing events if the repo is ambiguous. + +Integrated means the init runs, not that the package is listed. An init point +is a pair: the call, and the key it is constructed from. A repo can carry the +call while the key it names is defined nowhere — then the client is built from +an empty string and captures nothing, however complete the manifest looks. So +when you find an init that reads a variable, look for that same name in the +repo's committed env template or its build config. Found, or the project state +shows real events arriving: integrated. Named nowhere, or you cannot tell: +**queue `init`**. It re-checks the pair itself and leaves a complete init +alone, so queuing it when you are unsure costs one cheap task, while skipping +it on a keyless init costs the whole run. + +**2. Which uploader variant is this project — or none?** Read the manifests +and pick at most one, by this precedence (first match wins): + +- `react-native` or `expo` in `package.json` dependencies → `react-native`. + A React Native repo also carries `ios/Podfile`, an `.xcodeproj` and Gradle + files for its native shells, so this rule wins over every native marker + below. Picking `ios` or `android` for it uploads no JavaScript source maps, + and every JavaScript stack trace stays minified. +- `pubspec.yaml` → `flutter` +- an `.xcodeproj`, `Podfile`, or `Package.swift` → `ios` +- a Gradle build file that applies `com.android.application` or + `com.android.library`, or an `AndroidManifest.xml` → `android`. A Gradle + project without these markers is a JVM server: keep reading. +- `go.mod` → `go` +- `Cargo.toml` → `rust` +- `astro` in `package.json` dependencies → **none**. Astro is not supported by + the uploader: it inlines scripts below its asset limit into the HTML, so a + build routinely emits a `.map` with no `.js` beside it, and the upload step + then fails the whole build. This rule wins over every `package.json` match + below — an Astro project that also depends on `vite` is still **none**. +- otherwise read `package.json` dependencies, first match wins: + `nuxt` → `nuxt`; `next` → `nextjs`; + `@angular/core` → `angular`; `vite` → `vite`; `webpack` → `webpack`; + `rollup` → `rollup`; `react` → `react`; server-only Node → `node`; + any other browser JS → `web`. Server-only Node is `node` only when a + build compiles or bundles it (a `build` script running `tsc`, esbuild, or + another bundler), in this directory or in a package nested below it. + Server JavaScript that no build touches runs as written: **none**. +- **none** for platforms whose stack traces are already readable: plain + Python (Django, Flask, FastAPI), Ruby, PHP, Elixir, JVM servers, .NET. + Skip the whole upload subgraph for them — a skipped upload on such a + platform is an outcome, not a gap. Fact 3 decides what Python, Ruby, and + PHP get instead. + +When a variant matched, the uploader skill id is +`error-tracking-upload-source-maps-`. Pass it to the four upload +tasks as `inputs: { skillId: "", displayName: "" }` +so no task re-detects. + +**3. Which release-linking variant is this project — or none?** It applies +when the app itself is a server that runs Python, Ruby, or PHP source as is: + +- a Python app (`requirements*.txt`, `pyproject.toml`, `Pipfile`, or + `setup.py`; Django, Flask, FastAPI, and plain Python alike) → `python` +- a Ruby app (`Gemfile` or a `*.gemspec`; Rails, Sinatra, and plain Ruby + alike) → `ruby` +- a PHP app (`composer.json`; Laravel and plain PHP alike) → `php` +- **none** for every other platform. A manifest that only carries tooling for + another platform does not count: a `Gemfile` for CocoaPods or fastlane beside + a mobile app, or a `requirements.txt` for scripts beside a Node service. The + mobile uploader variants (`react-native`, `flutter`, `ios`, `android`) always + mean none here. Bundled and compiled platforms get their release from the + uploader, and Elixir, JVM servers, and .NET have no release-linking skill + yet. + +When a variant matched, the skill id is +`error-tracking-link-releases-`. Pass it to `link-releases` as +`inputs: { skillId: "", displayName: "" }`. Fact 3 +does not depend on fact 2: a Django app that also bundles a Vite frontend in +the same project gets both the upload subgraph and `link-releases`, and both +default to the same release. + +The three facts are independent — settle ALL of them before you enqueue +anything. "PostHog is already integrated" answers fact 1 only; it never decides +fact 2 or fact 3, and an already-integrated project still gets the upload +subgraph or `link-releases` when a variant matches. A compiled or bundled JS +project normally has an uploader variant: a Node service built with `tsc` ships +minified/compiled output, so it is the `node` variant, not "none". Only three +kinds of project skip the upload subgraph — the readable-stack platforms listed +above, Astro, and server JavaScript that no build compiles or bundles. + +Then seed the graph: + +- `install`, only when the SDK is missing from the manifest. +- `init`, independent of `install` — whenever fact 1 did not show a complete + pair. Do not stop on an uninstrumented repo, integrate. +- `capture-exceptions`, after whichever of `install` and `init` you queued + (with no dependencies when you queued neither). +- When an uploader variant matched, add the upload subgraph: + - `credentials`, no dependencies — it stops to ask the user for a personal + API key, so keep it a root task: the prompt reaches the user early while + the code tasks run. + - `configure`, after `capture-exceptions` — build-config changes; it runs + after the code edits so the two never fight over the same files. + - `wire-ci`, after `configure` and `credentials`. +- When a release-linking variant matched, `link-releases`, after + `capture-exceptions` — and after `wire-ci` too when you queued the upload + subgraph, because both edit the deploy pipeline. It changes production + deploys only, needs nothing from the user during the run, and leaves local + development as it is. +- When you queued the upload subgraph, `verify`, after every other task that + edits code: `capture-exceptions`, `configure`, `credentials`, and + `link-releases` when you queued it. It builds the project once every edit is + in, with the upload credentials in place, and fixes what broke. It does not + wait for `wire-ci`: that task edits only CI files, which a local build never + reads, so the two run side by side. A project without the upload subgraph + has no build of its own to check (Python, Ruby, and PHP apps build only in + their deploy), so it gets no `verify`. +- `report`, after every other queued task except `test-setup`. It writes the + handoff once the work is done, so it describes what actually shipped. +- When you queued the upload subgraph, `test-setup` last, after `report` — it + offers the user an optional local end-to-end test. The report comes first on + purpose: a user who walks away still gets it. + +A task can name another as a dependency only once that one is queued and has +an id. So queue the tail one call per turn, never batched: `verify` when you +queue it, then `report` naming the `verify` id, then `test-setup` naming the +`report` id. A `report` queued in the same turn as `verify` cannot wait for it, +and then reports a build that has not finished. + +Never plan an identify, capture, dashboard, or session-replay task — this run +sets up error tracking, not the full integration. The minimal SDK footprint +that `install` and `init` leave behind is enough for exceptions to flow. + +## How you know you succeeded + +Every task in the chosen graph is queued with that dependency shape, the four +upload tasks (when queued) share the same `{ skillId, displayName }` inputs, +`verify` (when queued) depends on every task that edits code except `wire-ci`, +`report` depends on every other task except `test-setup` (directly or +transitively), `test-setup` (when queued) depends on `report`, and the first +task is runnable. `link-releases` (when queued) carries its own +`{ skillId, displayName }` inputs. Your plan states all three facts explicitly: +whether PostHog was integrated — and, when you called it integrated, the name +of the key you found defined —, which uploader variant matched — or, when you +queue no upload tasks, why no variant applies: which readable-stack platform +this is, or that Astro is not supported by the uploader —, and which +release-linking variant matched, or that none applies. A plan that never +mentions fact 2 or fact 3 is an incomplete plan, not a decision. Keep labels +short — the action in a few words. diff --git a/src/programs/error-tracking/agent/test-setup.md b/src/programs/error-tracking/agent/test-setup.md new file mode 100644 index 000000000..facccf9be --- /dev/null +++ b/src/programs/error-tracking/agent/test-setup.md @@ -0,0 +1,55 @@ +--- +type: test-setup +flow: error-tracking +label: Offer an optional local test +sink: true +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask] +disallowedTools: [enqueue_task] +dependsOn: [report] +--- + +## Goal + +The setup is done and the report is written. Offer the user an optional, +one-time, end-to-end check that errors reach PostHog with readable stack traces. + +You run after the report on purpose. A user who stepped away still gets a +finished setup and a report, so nothing waits on this test. + +Ask before you do anything else, with `wizard_ask`: +`{ id: "test-affordance", prompt: "Error tracking is set up, and the report is in posthog-error-tracking-report.md. Optional: want me to help you test it locally? I'll add a temporary test button (or route) and build the app, so you can run it and confirm errors show up in Error Tracking with readable stack traces. Then I'll remove it.", kind: "single", options: [{ label: "Yes, help me test it", value: "yes" }, { label: "No, I'm done", value: "no" }] }` + +- **"no"**, no answer (`__cancelled__`), or `wizard_ask` unavailable: report + this task done right away, noting the test was offered and not taken. Install + no skill and read nothing first. +- **"yes"**: install the skill your task input names (`install_skill` with the + `skillId`) and follow its **"Test the local setup"** step for the + platform-appropriate affordance, the `captureException` shape, the + placement, and the read-before-edit / always-revert rules. Add the + affordance, then run the production build yourself: it uploads the maps for + the bundle that carries the affordance. + When the build fails, revert, tell the user what failed, and stop. When it + passes, pause with a second `wizard_ask` (id `"test-done"`, a single + `Continue (revert test code)` option) whose prompt gives the rest as literal + numbered steps: run the built app, trigger the affordance, then confirm the + error resolves to real source in Error Tracking. When the platform's build + and run are one step (an Xcode Run, `expo run`), give that step to the user + instead of building. After the user continues, or when that ask comes back + unanswered, REVERT every test edit per the skill's rules. Never leave the + affordance in place, even if the user says it didn't work. + +When the user says the test failed, add a short **"Local test"** section at the +end of `posthog-error-tracking-report.md` with what they saw, and say the same +in chat. The report task has already run, so this is the only place the +failure is written down. + +## How you know you succeeded + +Either the offer was declined or unanswered and no code changed, or the +affordance was added, tested, and fully reverted. Your handoff says which, and +a failure the user reported is in the report file. diff --git a/src/programs/error-tracking/agent/verify.md b/src/programs/error-tracking/agent/verify.md new file mode 100644 index 000000000..7cfa900a5 --- /dev/null +++ b/src/programs/error-tracking/agent/verify.md @@ -0,0 +1,82 @@ +--- +type: verify +flow: error-tracking +label: Build and check the setup +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, detect_package_manager] +disallowedTools: [enqueue_task] +dependsOn: [capture-exceptions, configure, credentials, link-releases] +--- + +## Goal + +Every edit of this run is in. Prove the setup works by building the project +the way it builds for production. `read_handoffs` names the files each task +changed, the packages it added, and the env variable names it used. + +Read the handoffs first. When the `configure` handoff reports a passing +production build that ran with the upload credentials present, and this run +has no `link-releases` task, that build already ran on the finished code: every +other code task finished before `configure` started. Do not build again. Hand +off its command, result, and uploader summary lines as your own, and say you +reused them. Otherwise: + +1. Install with the project's package manager, so the build sees the manifest + and lockfile this run left behind. In a workspace package, that is the + manager whose lockfile sits at the workspace root above this directory. +2. Run the production build the project defines in this checkout: its package + manager's build script, or the platform's compiler (Cargo, Go, Gradle, + Xcode). You cannot run a Dockerfile or a CI job; when that is the only + build, say so and finish. When the project defines no build, run its + typecheck or lint script instead; when it defines none of them, say so and + finish. +3. Read the exit status first, then the log. When source-map or debug-symbol + upload is wired and the credentials are written, the log shows the uploader + creating a release and uploading files. A green build with no upload lines + means the upload never ran: compare the variable names the build config + reads with the names in the env file (`check_env_keys`). When the + `credentials` handoff reports no key, the build still has to pass, with the + upload skipped. +4. Fix what this run's changes broke, in the files the handoffs name, and build + again. A failure that was there before this run is not yours: say so in your + handoff and leave it. After a few attempts, stop and hand off what still + fails. + +Build, never run. A command that installs or launches the app (`expo run:*`, +`react-native run-*`, `flutter run`, `./gradlew install*`) opens a simulator +or an emulator on the user's machine. On an Expo app whose `android/` and +`ios/` folders are generated, build the JavaScript bundle instead: `npx expo +export --source-maps --platform `. + +When the upload is a separate `posthog-cli` command rather than part of the +build, or the build sits in a `make` target or a shell script, you cannot run +those: run the build command itself, and say so. Never add a package script to +get past a blocked command. Your shell gets a reduced +environment: `ANDROID_HOME` and other variables from the user's shell do not +reach it. A build that cannot find an SDK (`SDK location not found`) failed +because of this shell, not the project: do not retry it, and do not give the +user a setup step for it. A missing toolchain, or a local interpreter too old +for a version this run pinned, is the environment, not the setup: say so and +finish, and never lower a version to make a local install pass. + +Finish this task `done` even when the build still fails. Your handoff gives +the command, the error, and whether this run caused it, and the report turns +it into a follow-up. A `failed` verify stops the report, and the user gets no +report at all. + +A build with the credentials in place creates a release and uploads to the +user's project. That is expected: it is what every production build will do, +and it puts the first symbol set in PostHog right away. + +## How you know you succeeded + +The production build passes with this run's changes, and when the upload is +wired with credentials, its log shows the release and the upload. Your handoff +gives the command you ran, or that you reused `configure`'s build, its result, +the uploader's summary lines (release name and how many files it uploaded), +every fix you made with its file, and anything that still fails, so the report +can tell the user what was verified. The task ends `done` either way. diff --git a/src/programs/error-tracking/agent/wire-ci.md b/src/programs/error-tracking/agent/wire-ci.md new file mode 100644 index 000000000..ea5f4f3e8 --- /dev/null +++ b/src/programs/error-tracking/agent/wire-ci.md @@ -0,0 +1,109 @@ +--- +type: wire-ci +flow: error-tracking +label: Set up CI for automatic uploads +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill] +disallowedTools: [enqueue_task] +dependsOn: [configure, credentials] +--- + +## Goal + +Make the credentials reach the production build wherever it actually runs, so +source maps upload on every deploy — not just on a local build. Install the +skill your task input names (`install_skill` with the `skillId`) and follow its +**"Set up CI for automatic uploads"** step — it owns tracing where the +production build runs and wiring the credentials through every layer, whatever +the CI provider. + +The `configure` and `credentials` handoffs already name the build-config keys +and the exact environment-variable names in use — carry those same names into +the pipeline; do not invent new ones. Trace the deploy path by reading the +project's own files (CI workflows, Dockerfiles, deploy scripts) — never invent +config that is not there. You cannot create the CI secret that holds the API +key; reference it by name and carry that follow-up, plus any deploy path you +could not trace, into your handoff for the report. + +A package inside a workspace rarely holds its own pipeline. The CI config and +deploy files usually sit at the repository root, above this directory +(`../.github/workflows`, `../../.gitlab-ci.yml`): look there before you +conclude there is none, and wire the job that builds this package. When you +cannot open them, name the file and the job in your handoff. + +The user merges this change before they create the secrets. A step that fails +when a PostHog variable is empty (`${VAR:?…}`, an `exit 1`, a required input) +breaks every deploy until they do. Pass the variables through and let the +upload skip itself when they are empty: the build config's gate does that, and a +`posthog-cli` step in the pipeline needs a condition on the key. Name the +secrets in your handoff instead of enforcing them. + +## Two things cross the boundary, not one + +Credentials are half of it. The uploader also needs a **release identity** — a +name and a version — and it derives that from the CI's own git variables or +from a `.git` directory. A container build sees neither: `.git` is almost +always in `.dockerignore`, and the CI's variables stop at the `docker build` +command. Nor does a hardcoded release name rescue you; the uploader wants both +halves, and stops the build when it has only one. + +So forward the provider's git variables into the build the same way you forward +credentials, and declare each one as `ARG` **and** `ENV` — `ARG` alone is not +visible to the uploader's environment lookup. Your skill's "Associate the +release with a git commit" step lists the variables per provider. Read that +step even though it is not the CI step: this boundary is where it applies. + +Forward them only where a provider actually sets them. A project with no +pipeline — an image built and run by hand — has nothing to inherit from, and +variables declared but never filled resolve to no release at all: it reads as +wired and still fails the build. There the identity has to be supplied outright, +both halves, from something the build itself holds — a build argument the +operator passes, or the manifest's own version — so name and version are always +present. Decide which case you are in by reading the repo, not by assuming a +provider exists. + +That `ARG`-plus-`ENV` shape is for the git variables and the non-secret settings +only. The API key is a secret and keeps whatever secret-carrying mechanism the +build system offers — a build secret mounted for the one step that needs it, a +masked variable, a secret file. A secret in `ARG` or `ENV` is recorded in the +build history, and the builder itself will warn you: *do not use ARG or ENV +instructions for sensitive data*. Widening the git-variable pattern to cover the +key is a downgrade, not consistency. + +A build cache between the pipeline and the build, such as Turborepo's, is one +more boundary. List the upload credentials among the task's hashed inputs +(Turborepo's `env`), so a build that has them never replays an entry made +without them, and pass the git variables through without hashing them +(`passThroughEnv`). Leave the cache on: a hit restores output whose chunks are +already uploaded. + +## The runtime boundary has names too + +Upload credentials are a build-time concern. The app also reads its own +variables at **run** time, and the step that starts it is another place a name +has to match — a `docker run -e`, a compose file, a systemd unit, a platform's +environment settings. That step was written before PostHog existed here, so the +name it passes is whatever the project used back then. If `init` settled on a +different one, the deployed process starts with an undefined key. + +Nothing fails loudly. In production the init guard returns quietly instead of +throwing, so the process boots, serves traffic, and reports nothing — the same +silent shape as a mismatched upload variable, one boundary later. + +So read the names the app's own source reads, and make the start step pass +exactly those. Rename the deploy step's variable, not the code: the code is the +half that already exists. Name any secret the user must create under its new +name in your handoff. + +## How you know you succeeded + +The pipeline that runs the production build carries the upload credentials by +the same names the credentials task used and reaches the uploader with a +resolvable release identity, and the step that starts the app passes the +variable names the app's own source reads. Every secret the user still has to create is named +in your handoff. Your handoff lists the CI files you changed +and every manual follow-up, so the report can hand them to the user. diff --git a/src/programs/error-tracking/flow.ts b/src/programs/error-tracking/flow.ts new file mode 100644 index 000000000..aa684ba89 --- /dev/null +++ b/src/programs/error-tracking/flow.ts @@ -0,0 +1,37 @@ +import { defineFlow } from '@programs/program-contract'; + +/** + * Error tracking. The planner picks the upload subgraph and release linking + * from the platform it finds; the graph fixes where each sits. + */ +export const flow = defineFlow({ + sink: 'test-setup', + nodes: { + install: { picked: true }, + init: { picked: true }, + 'capture-exceptions': { dependsOn: ['install', 'init'] }, + // A root task: it asks for a key, so the question lands while code tasks run. + credentials: { picked: true }, + configure: { picked: true, dependsOn: ['capture-exceptions'] }, + 'wire-ci': { picked: true, dependsOn: ['configure', 'credentials'] }, + // After wire-ci too, because both edit the deploy pipeline. + 'link-releases': { + picked: true, + dependsOn: ['capture-exceptions', 'wire-ci'], + }, + // Builds once every code edit is in; wire-ci only edits CI files, which a local build never reads. + verify: { + picked: true, + dependsOn: [ + 'capture-exceptions', + 'configure', + 'credentials', + 'link-releases', + ], + }, + report: { + dependsOn: ['capture-exceptions', 'wire-ci', 'link-releases', 'verify'], + }, + 'test-setup': { picked: true, dependsOn: ['report'] }, + }, +}); diff --git a/src/programs/error-tracking/index.ts b/src/programs/error-tracking/index.ts index 37881b02d..eeea5725b 100644 --- a/src/programs/error-tracking/index.ts +++ b/src/programs/error-tracking/index.ts @@ -25,6 +25,7 @@ import { analytics } from '@utils/analytics'; import { ProgramAbort } from '@programs/program-contract'; import { ErrorCodes } from '@shared/errors'; import { abortNoFrameworkDetected } from '../shared/abort-no-framework'; +import { flow } from './flow'; const ERROR_TRACKING_REPORT_FILE = 'posthog-error-tracking-report.md'; const ERROR_TRACKING_DOCS_URL = 'https://posthog.com/docs/error-tracking'; @@ -173,6 +174,8 @@ export const config: ProgramConfig = { 'Set up PostHog error tracking, source-map upload and release linking included', id: 'error-tracking', agentFlow: 'error-tracking', + agentPrompts: () => import('./agent-prompts').then((m) => m.AGENT_PROMPTS), + flow, reportFile: ERROR_TRACKING_REPORT_FILE, // The run is scoped to the project the user picks after login. runSteps: { diff --git a/src/programs/feature-flags/agent-prompts.ts b/src/programs/feature-flags/agent-prompts.ts new file mode 100644 index 000000000..b8366dce5 --- /dev/null +++ b/src/programs/feature-flags/agent-prompts.ts @@ -0,0 +1,16 @@ +// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. + +/** This program's agent prompts by task type, verbatim. */ +export const AGENT_PROMPTS: Readonly> = { + 'create-flag': + "---\ntype: create-flag\nflow: feature-flags\nlabel: Create the example flags in PostHog\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-mcp]\nallowedTools: [Read, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure this PostHog project has one example flag for each side in your\n`sides` input, and none for a side that is not listed:\n\n| Side | Key | Name |\n|---|---|---|\n| `backend` | `wizard-example-backend-flag` | Wizard example backend flag |\n| `frontend` | `wizard-example-frontend-flag` | Wizard example frontend flag |\n\nFor each flag you need:\n\n1. Look it up by key with `feature-flag-get-definition-by-key`. One keyed\n lookup per flag; never list every flag in the project.\n2. Found: reuse it as it is. Do not change its rollout or its active state.\n3. Not found: create it with `create-feature-flag`, using the key and name\n above, a boolean flag with one release condition at 0% rollout, and\n `active: false`. New flags are active by default, so pass\n `active: false` explicitly. The user turns it on when they are ready.\n4. Create rejected because the key already exists: another run created it\n after your lookup. Look it up by key once more and reuse it as in step 2.\n\nRun `info` on each tool before calling it; never guess the input shape.\n\nIf a call is rejected for permissions, or the tool is hidden for a missing\nscope, stop and say so in your handoff: the credentials need\n`feature_flag:read` and `feature_flag:write`. Do not retry with other\ntools.\n\n## How you know you succeeded\n\nYour handoff gives, for each flag: its side, key, id, whether you created or\nreused it, whether it is active, and its URL in PostHog (the tool result's\nURL, verbatim).\n", + evaluate: + "---\ntype: evaluate\nflow: feature-flags\nlabel: Evaluate the flags in the app\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-feature-flags-step]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nFor each flag the create-flag handoff names, add one evaluation on that\nflag's side of this app: the backend flag in server code through the server\nSDK, the frontend flag in browser or device code through the client SDK.\nNever evaluate a flag on the other side. Your skill carries the SDK flag docs\nand the best-practices page; follow them for the exact calls.\n\nThe SDKs are installed and initialized, either already or by the tasks before\nyou. Build on that; do not re-check it.\n\nFor each flag, choose a call site where a real request or screen passes\nthrough. On the backend, evaluate once for that request and pass the value\ndown; never re-evaluate deeper in the call stack. Put the keys in a constants\nmodule the call site imports, one module per language; a template script that\ncannot import it gets the key from the server's constant, never a literal.\nEvaluate with the identified user's distinct id, the same id on both sides,\nand report each evaluation back to PostHog the way the docs show for that SDK.\nGate something harmless on each value, such as a log line or a small label,\nso the app behaves the same while the flags are off.\n\nThis is an edit-only task. Do not install dependencies, run the build, or\nstart the app.\n\n## How you know you succeeded\n\nEach flag is evaluated once, at one call site on its own side, with its key\nimported from a constants module, and its evaluation is reported. Your\nhandoff names, for each flag, the files changed and the call site, plus how\nthe distinct id is chosen on each side.\n", + init: "---\ntype: init\nflow: feature-flags\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep, check_env_keys, set_env_values]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure PostHog is initialized on each side in your `sides` input. An\nexisting init counts only when the env variable it reads is defined: check it\nwith `check_env_keys`. When a side's init is complete, leave it alone and\nsay so. Otherwise create or finish it following your skill, including the\nenv wiring and `.env.example`. A template-script frontend gets the PostHog\nsnippet in its base template.\n\nInitialize each SDK so a flag can be evaluated, and stop. Do not evaluate any\nflag; the evaluate task owns that.\n\nYou do not install packages, run builds, linters, or tests, or start the app.\nThe install task owns packages, and your edits just need to be right by\nreading.\n\n## How you know you succeeded\n\nPostHog initializes from a defined key on every side in `sides`. Your\nhandoff names, for each side, the init file, the client the evaluate task\nshould use, and the env variable names (never values).\n", + install: + "---\ntype: install\nflow: feature-flags\nlabel: Add the PostHog SDKs to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure each side in your `sides` input has its PostHog SDK in the\nmanifest: the server library for `backend`, the browser or mobile library\nfor `frontend`. Leave an SDK that is already there alone and say so.\nInstall a missing one following your skill, which owns the package manager\nand version rules.\n\nA frontend that is only scripts in server-rendered templates has no package\nto install: the init task adds the PostHog snippet there. Install nothing for\nthat side, and install nothing beyond these SDKs.\n\n## How you know you succeeded\n\nEach side's SDK is declared in the manifest at a real version, or your\nhandoff says plainly why the environment stopped you. Your handoff names the\nmanifest and package for each side.\n", + report: + "---\ntype: report\nflow: feature-flags\nlabel: Write the setup report\nsink: true\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-notebook, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nCompose the setup report from `read_handoffs` only, then publish it with a\nsingle `publish_handoff` call. The same call mirrors the markdown into a\nshareable PostHog notebook. The notebook skill says what the result means and\ncovers older wizards. Do not write a report file. The handoff call and the\nnotebook are how the report reaches the user.\n\nWrite one section per flag:\n\n- The flag: its side, key, URL, whether it was created or reused, and whether\n it is active, as the handoff gives it. Say a flag stays off until the user\n enables it only when the handoff marks it inactive.\n- Where the app evaluates it: files and call site, the constants module, and\n how the distinct id is chosen.\n\nThen:\n\n- If the run installed or initialized an SDK, say which and on which side.\n- How to confirm it works: open the page or endpoint that evaluates each flag,\n then find the evaluation on the flag's page in PostHog. It can take a few\n minutes to show up.\n\nThe run does not check that an evaluation reached PostHog, so never write that\none did. Where a step failed or was skipped, say so plainly instead of rounding\nit up to success.\n\n## How you know you succeeded\n\nOne `publish_handoff` call went through with the full report, and a user who\nreads only it knows which flags were created, which side evaluates each and\nwhere, which are still off and need turning on, and how to confirm an\nevaluation reaches PostHog. Every claim in it traces to a handoff. The notebook\nmirror is best effort: when it was created, the user has its link, and when it\nwasn't, your final message says why. A missing notebook never fails this task.\n", + 'setup-feature-flags': + '---\ntype: setup-feature-flags\nflow: feature-flags\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan example PostHog feature flags for this project and seed the task queue.\nThe end state: one example flag per side of the app that exists, each\nevaluated once in one place on its own side and reported back to PostHog, and\na report in a PostHog notebook telling the user how to turn the flags on.\n\nFirst establish two facts from the repo.\n\n**1. Which sides does the app have?**\n\n- `backend`: server code that handles requests, such as route handlers, API\n views, controllers, server components, or server loaders.\n- `frontend`: JavaScript or device code the app owns that runs in a browser\n or on a device, such as client components, a single-page app, inline\n scripts in its page templates, script files its pages load (directly or\n through asset helpers such as import maps or Vite), or mobile screens.\n\nA side counts only when the app already has code there to put an evaluation\nin. Never add a side to host a flag. Server-rendered templates that load no\nJavaScript of the app\'s own are not a frontend: such an app has only a\nbackend. An API has only a backend; a mobile app or a static single-page app\nhas only a frontend.\n\n**2. Is PostHog already integrated on each of those sides?** Look for the\nPostHog SDK for that side in the dependency manifests (or, for template\nscripts, the PostHog snippet) and an init call in the source. An init counts\nonly when the key it reads is defined in the repo\'s committed env template or\nbuild config. When you cannot tell, treat it as missing: the init task\nre-checks and leaves a complete init alone.\n\nThen seed the graph. Give every task the same input: `sides`, the list of\nsides you found (`["backend", "frontend"]`, `["backend"]`, or\n`["frontend"]`). Pass no other inputs; each task learns the rest from the\nhandoffs before it.\n\n- `install`, only when an SDK a side needs is missing from the manifest.\n- `init`, whenever an init is missing or unproven on either side.\n- `create-flag`, with no dependencies. It only talks to PostHog.\n- `evaluate`, after `create-flag` and after whichever of `install` and\n `init` you queued.\n- `report`, after every other task.\n\nNever plan capture, dashboard, or session-replay work. This run sets up the\nexample flags, nothing else.\n\n## How you know you succeeded\n\nEvery task above is queued with that dependency shape and the `sides` input,\n`report` depends on every other task, and your plan states which sides the\napp has, whether PostHog is integrated on each, and the files that told you.\nKeep labels short.\n', +}; diff --git a/src/programs/feature-flags/agent/create-flag.md b/src/programs/feature-flags/agent/create-flag.md new file mode 100644 index 000000000..04bbefd39 --- /dev/null +++ b/src/programs/feature-flags/agent/create-flag.md @@ -0,0 +1,48 @@ +--- +type: create-flag +flow: feature-flags +label: Create the example flags in PostHog +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-mcp] +allowedTools: [Read, posthog_exec] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure this PostHog project has one example flag for each side in your +`sides` input, and none for a side that is not listed: + +| Side | Key | Name | +|---|---|---| +| `backend` | `wizard-example-backend-flag` | Wizard example backend flag | +| `frontend` | `wizard-example-frontend-flag` | Wizard example frontend flag | + +For each flag you need: + +1. Look it up by key with `feature-flag-get-definition-by-key`. One keyed + lookup per flag; never list every flag in the project. +2. Found: reuse it as it is. Do not change its rollout or its active state. +3. Not found: create it with `create-feature-flag`, using the key and name + above, a boolean flag with one release condition at 0% rollout, and + `active: false`. New flags are active by default, so pass + `active: false` explicitly. The user turns it on when they are ready. +4. Create rejected because the key already exists: another run created it + after your lookup. Look it up by key once more and reuse it as in step 2. + +Run `info` on each tool before calling it; never guess the input shape. + +If a call is rejected for permissions, or the tool is hidden for a missing +scope, stop and say so in your handoff: the credentials need +`feature_flag:read` and `feature_flag:write`. Do not retry with other +tools. + +## How you know you succeeded + +Your handoff gives, for each flag: its side, key, id, whether you created or +reused it, whether it is active, and its URL in PostHog (the tool result's +URL, verbatim). diff --git a/src/programs/feature-flags/agent/evaluate.md b/src/programs/feature-flags/agent/evaluate.md new file mode 100644 index 000000000..83aab53f1 --- /dev/null +++ b/src/programs/feature-flags/agent/evaluate.md @@ -0,0 +1,44 @@ +--- +type: evaluate +flow: feature-flags +label: Evaluate the flags in the app +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-feature-flags-step] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +For each flag the create-flag handoff names, add one evaluation on that +flag's side of this app: the backend flag in server code through the server +SDK, the frontend flag in browser or device code through the client SDK. +Never evaluate a flag on the other side. Your skill carries the SDK flag docs +and the best-practices page; follow them for the exact calls. + +The SDKs are installed and initialized, either already or by the tasks before +you. Build on that; do not re-check it. + +For each flag, choose a call site where a real request or screen passes +through. On the backend, evaluate once for that request and pass the value +down; never re-evaluate deeper in the call stack. Put the keys in a constants +module the call site imports, one module per language; a template script that +cannot import it gets the key from the server's constant, never a literal. +Evaluate with the identified user's distinct id, the same id on both sides, +and report each evaluation back to PostHog the way the docs show for that SDK. +Gate something harmless on each value, such as a log line or a small label, +so the app behaves the same while the flags are off. + +This is an edit-only task. Do not install dependencies, run the build, or +start the app. + +## How you know you succeeded + +Each flag is evaluated once, at one call site on its own side, with its key +imported from a constants module, and its evaluation is reported. Your +handoff names, for each flag, the files changed and the call site, plus how +the distinct id is chosen on each side. diff --git a/src/programs/feature-flags/agent/init.md b/src/programs/feature-flags/agent/init.md new file mode 100644 index 000000000..516bdbd6c --- /dev/null +++ b/src/programs/feature-flags/agent/init.md @@ -0,0 +1,35 @@ +--- +type: init +flow: feature-flags +label: Set up PostHog initialization +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-init, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep, check_env_keys, set_env_values] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure PostHog is initialized on each side in your `sides` input. An +existing init counts only when the env variable it reads is defined: check it +with `check_env_keys`. When a side's init is complete, leave it alone and +say so. Otherwise create or finish it following your skill, including the +env wiring and `.env.example`. A template-script frontend gets the PostHog +snippet in its base template. + +Initialize each SDK so a flag can be evaluated, and stop. Do not evaluate any +flag; the evaluate task owns that. + +You do not install packages, run builds, linters, or tests, or start the app. +The install task owns packages, and your edits just need to be right by +reading. + +## How you know you succeeded + +PostHog initializes from a defined key on every side in `sides`. Your +handoff names, for each side, the init file, the client the evaluate task +should use, and the env variable names (never values). diff --git a/src/programs/feature-flags/agent/install.md b/src/programs/feature-flags/agent/install.md new file mode 100644 index 000000000..950dd81f7 --- /dev/null +++ b/src/programs/feature-flags/agent/install.md @@ -0,0 +1,31 @@ +--- +type: install +flow: feature-flags +label: Add the PostHog SDKs to the manifest +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-install] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure each side in your `sides` input has its PostHog SDK in the +manifest: the server library for `backend`, the browser or mobile library +for `frontend`. Leave an SDK that is already there alone and say so. +Install a missing one following your skill, which owns the package manager +and version rules. + +A frontend that is only scripts in server-rendered templates has no package +to install: the init task adds the PostHog snippet there. Install nothing for +that side, and install nothing beyond these SDKs. + +## How you know you succeeded + +Each side's SDK is declared in the manifest at a real version, or your +handoff says plainly why the environment stopped you. Your handoff names the +manifest and package for each side. diff --git a/src/programs/feature-flags/agent/report.md b/src/programs/feature-flags/agent/report.md new file mode 100644 index 000000000..340ed97d2 --- /dev/null +++ b/src/programs/feature-flags/agent/report.md @@ -0,0 +1,50 @@ +--- +type: report +flow: feature-flags +label: Write the setup report +sink: true +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-notebook, integration-v2-mcp] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Compose the setup report from `read_handoffs` only, then publish it with a +single `publish_handoff` call. The same call mirrors the markdown into a +shareable PostHog notebook. The notebook skill says what the result means and +covers older wizards. Do not write a report file. The handoff call and the +notebook are how the report reaches the user. + +Write one section per flag: + +- The flag: its side, key, URL, whether it was created or reused, and whether + it is active, as the handoff gives it. Say a flag stays off until the user + enables it only when the handoff marks it inactive. +- Where the app evaluates it: files and call site, the constants module, and + how the distinct id is chosen. + +Then: + +- If the run installed or initialized an SDK, say which and on which side. +- How to confirm it works: open the page or endpoint that evaluates each flag, + then find the evaluation on the flag's page in PostHog. It can take a few + minutes to show up. + +The run does not check that an evaluation reached PostHog, so never write that +one did. Where a step failed or was skipped, say so plainly instead of rounding +it up to success. + +## How you know you succeeded + +One `publish_handoff` call went through with the full report, and a user who +reads only it knows which flags were created, which side evaluates each and +where, which are still off and need turning on, and how to confirm an +evaluation reaches PostHog. Every claim in it traces to a handoff. The notebook +mirror is best effort: when it was created, the user has its link, and when it +wasn't, your final message says why. A missing notebook never fails this task. diff --git a/src/programs/feature-flags/agent/setup-feature-flags.md b/src/programs/feature-flags/agent/setup-feature-flags.md new file mode 100644 index 000000000..b46e90946 --- /dev/null +++ b/src/programs/feature-flags/agent/setup-feature-flags.md @@ -0,0 +1,66 @@ +--- +type: setup-feature-flags +flow: feature-flags +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Glob, Grep] +disallowedTools: [Write, Edit, Bash, complete_task] +dependsOn: [] +--- + +## Goal + +Plan example PostHog feature flags for this project and seed the task queue. +The end state: one example flag per side of the app that exists, each +evaluated once in one place on its own side and reported back to PostHog, and +a report in a PostHog notebook telling the user how to turn the flags on. + +First establish two facts from the repo. + +**1. Which sides does the app have?** + +- `backend`: server code that handles requests, such as route handlers, API + views, controllers, server components, or server loaders. +- `frontend`: JavaScript or device code the app owns that runs in a browser + or on a device, such as client components, a single-page app, inline + scripts in its page templates, script files its pages load (directly or + through asset helpers such as import maps or Vite), or mobile screens. + +A side counts only when the app already has code there to put an evaluation +in. Never add a side to host a flag. Server-rendered templates that load no +JavaScript of the app's own are not a frontend: such an app has only a +backend. An API has only a backend; a mobile app or a static single-page app +has only a frontend. + +**2. Is PostHog already integrated on each of those sides?** Look for the +PostHog SDK for that side in the dependency manifests (or, for template +scripts, the PostHog snippet) and an init call in the source. An init counts +only when the key it reads is defined in the repo's committed env template or +build config. When you cannot tell, treat it as missing: the init task +re-checks and leaves a complete init alone. + +Then seed the graph. Give every task the same input: `sides`, the list of +sides you found (`["backend", "frontend"]`, `["backend"]`, or +`["frontend"]`). Pass no other inputs; each task learns the rest from the +handoffs before it. + +- `install`, only when an SDK a side needs is missing from the manifest. +- `init`, whenever an init is missing or unproven on either side. +- `create-flag`, with no dependencies. It only talks to PostHog. +- `evaluate`, after `create-flag` and after whichever of `install` and + `init` you queued. +- `report`, after every other task. + +Never plan capture, dashboard, or session-replay work. This run sets up the +example flags, nothing else. + +## How you know you succeeded + +Every task above is queued with that dependency shape and the `sides` input, +`report` depends on every other task, and your plan states which sides the +app has, whether PostHog is integrated on each, and the files that told you. +Keep labels short. diff --git a/src/programs/feature-flags/flow.ts b/src/programs/feature-flags/flow.ts new file mode 100644 index 000000000..8382d004a --- /dev/null +++ b/src/programs/feature-flags/flow.ts @@ -0,0 +1,13 @@ +import { defineFlow } from '@programs/program-contract'; + +/** Example flags: the planner queues install and init only where an SDK or its init is missing. */ +export const flow = defineFlow({ + sink: 'report', + nodes: { + install: { picked: true }, + init: { picked: true }, + 'create-flag': {}, + evaluate: { dependsOn: ['create-flag', 'install', 'init'] }, + report: { dependsOn: ['evaluate'] }, + }, +}); diff --git a/src/programs/feature-flags/index.ts b/src/programs/feature-flags/index.ts index da40e603c..fb24edd8c 100644 --- a/src/programs/feature-flags/index.ts +++ b/src/programs/feature-flags/index.ts @@ -20,6 +20,7 @@ import type { ProgramSession } from '@programs/program-contract'; import type { CiRunnerContext } from '@programs/program-contract'; import { abortNoFrameworkDetected } from '../shared/abort-no-framework'; import { FEATURE_FLAGS_SCOPE_ADDITIONS } from './scopes.js'; +import { flow } from './flow'; const FEATURE_FLAGS_REPORT_FILE = 'posthog-feature-flags-report.md'; const FEATURE_FLAGS_DOCS_URL = 'https://posthog.com/docs/feature-flags'; @@ -54,6 +55,8 @@ export const config: ProgramConfig = { model: DEFAULT_AGENT_MODEL, }, agentFlow: 'feature-flags', + agentPrompts: () => import('./agent-prompts').then((m) => m.AGENT_PROMPTS), + flow, handoffNotebook: true, // The intro blocks Continue until a supported framework is detected. onReady: (ctx) => detectPostHogIntegration(ctx), diff --git a/src/programs/metrics/agent-prompts.ts b/src/programs/metrics/agent-prompts.ts new file mode 100644 index 000000000..d5103fc9b --- /dev/null +++ b/src/programs/metrics/agent-prompts.ts @@ -0,0 +1,13 @@ +// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. + +/** This program's agent prompts by task type, verbatim. */ +export const AGENT_PROMPTS: Readonly> = { + 'instrument-metrics': + '---\ntype: instrument-metrics\nflow: metrics\nlabel: Instrument the service with metrics\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [verify-sdk]\n---\n\n## Goal\n\nInstrument your service with application metrics. When your task input names\na `package`, stay inside it — sibling packages belong to other chains.\nInstall the platform\'s skill the way the verify handoff names it (`load_skill_menu` category\n"metrics", then `install_skill`), and follow it end-to-end — it owns where\nmetrics belong (request middleware, background\njobs, external calls, business commit sites), which of counter, gauge, and\nhistogram fits each site, and the low-cardinality attribute rules.\n\nImport the client the way the verify handoff names it. Only additive changes:\nadd metric calls, never restructure the code they land in.\n\n## How you know you succeeded\n\nThe hot paths and business commit sites record metrics through the one shared\nclient, the project still builds, and your handoff lists every metric name\nwith its type and where it fires.\n', + report: + "---\ntype: report\nflow: metrics\nlabel: Report and hand off\nsink: true\nmodel_pi: openai/gpt-5.6-luna\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-notebook]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: [instrument-metrics]\n---\n\n## Goal\n\nTell the user what their service now measures, from the handoffs of every task\nin the run. Briefly and concretely:\n\n- Each metric now recording: its name, type (counter, gauge, histogram), and\n where in the code it fires.\n- The service name the client reports under, and the SDK version the run\n verified or installed.\n- Anything skipped or deferred, with the reason — an environment that blocked\n an upgrade is an outcome, not an apology.\n- Where results appear: the Metrics page in PostHog, with the first series\n arriving as traffic flows.\n\nIf the run also installed or initialized the SDK, say so — the user started\nthis command without PostHog and now has it.\n\nPublish the report with a single `publish_handoff` call. The same call mirrors\nthe markdown into a shareable PostHog notebook. The notebook skill says what the result means and covers\nolder wizards. Do not write a report file. The handoff call and the notebook\nare how the report reaches the user.\n\n## How you know you succeeded\n\nA user who reads only your report knows what their service measures, where\neach number comes from, what still needs them, and where to look first. The\nreport went through one `publish_handoff` call. The notebook mirror is best\neffort: when it was created, the user has its link, and when it wasn't, your\nfinal message says why, and when an update failed, that the notebook holds the earlier version. A missing notebook never fails this task.\n", + 'setup-metrics': + '---\ntype: setup-metrics\nflow: metrics\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan an application-metrics setup and seed the task queue. Metrics are a\nper-service concern: every service gets its own SDK install and its own\n`service_name`.\n\nFirst map the repo — mechanically, before any judgment. Run one `find` per\nmanifest pattern across the whole tree, every time, even when the root looks\nlike a plain npm workspace (repos mix ecosystems, and the workspace list in a\nroot manifest is not the whole map):\n\n **/package.json **/pyproject.toml **/requirements.txt **/Pipfile\n **/go.mod **/composer.json **/Gemfile\n\nEvery directory holding a hit is a package. Classify each: a **service** runs\nserver-side work (server entrypoints, workers, APIs — whatever the language);\nbrowser-only apps and libraries are not services and get no chain (the report\nnotes them). Ignore installed-dependency and scaffolding directories\n(`node_modules`, `.claude`, caches) — they are not packages.\n\nThen seed, per service:\n\n- `verify-sdk`, with no dependencies — it settles that service\'s SDK question\n (installed, new enough, initialized for metrics) before any\n instrumentation. Pass `inputs: { package: "", service_name: "" }`\n so the task knows which package it owns; omit `package` in a single-package\n repo.\n- `instrument-metrics`, after that service\'s `verify-sdk`, with the same\n inputs.\n\nChains for different services are independent — do not wire edges between\nthem, so they run in parallel. Last, one `report` that depends on every\n`instrument-metrics`. It writes the handoff last, so it describes what\nactually shipped.\n\nMetrics ride each service\'s existing PostHog SDK (`posthog.metrics`), so the\ngraph is the same whether or not the repo has PostHog today — `verify-sdk`\nabsorbs the difference. Never plan an identify, capture, or dashboard task:\nthis run sets up application metrics, not the full integration.\n\n## How you know you succeeded\n\nEvery service has its own verify → instrument chain carrying its package and\nservice name, the chains share no edges, the report depends on all of them,\nand every `verify-sdk` is runnable. Keep labels short — the action in a few\nwords, naming the service when there are several.\n', + 'verify-sdk': + "---\ntype: verify-sdk\nflow: metrics\nlabel: Verify the PostHog SDK is ready for metrics\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Edit, Glob, Grep, Bash, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nLeave your service with a PostHog SDK that can record metrics. When your\ntask input names a `package`, that package is your whole world — its\nmanifest, its init site, its `service_name`; do not touch sibling packages.\nFirst call\n`load_skill_menu` with `category: \"metrics\"` and `install_skill` for the\nvariant matching the project's platform — the menu is the source of truth,\none variant per platform, and a full-stack app takes the server variant\nbecause metrics measure service work. The installed skill carries the\nplatform's install and init specifics. Then three checks, in order:\n\n1. **Installed?** Look for the SDK in the dependency manifest. Missing →\n install it with the project's own package manager and let it resolve the\n version — never invent one.\n2. **New enough?** `posthog.metrics` ships in recent SDK releases only — the\n skill states the version floor for this platform. Below it → bump with the\n package manager's upgrade command. At or above → leave it alone and say so\n in your handoff.\n3. **Initialized for metrics?** Find where the client is constructed. Add the\n `metrics` config (a service name) to that existing client, as the skill\n shows. No client anywhere → initialize one, following the skill. Never\n construct a second client beside a working one, and never touch identify\n calls, event capture, or other init options.\n\n## How you know you succeeded\n\nThe manifest holds the SDK at a metrics-capable version and exactly one client\ninit carries the metrics config — or your handoff plainly says why the\nenvironment stopped you. Name the skill variant you installed, the manifest, the version, and the\ninit site in your handoff, so instrumentation installs the same skill and\nknows what to import and where.\n", +}; diff --git a/src/programs/metrics/agent/instrument-metrics.md b/src/programs/metrics/agent/instrument-metrics.md new file mode 100644 index 000000000..4030ceea3 --- /dev/null +++ b/src/programs/metrics/agent/instrument-metrics.md @@ -0,0 +1,32 @@ +--- +type: instrument-metrics +flow: metrics +label: Instrument the service with metrics +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill] +disallowedTools: [enqueue_task] +dependsOn: [verify-sdk] +--- + +## Goal + +Instrument your service with application metrics. When your task input names +a `package`, stay inside it — sibling packages belong to other chains. +Install the platform's skill the way the verify handoff names it (`load_skill_menu` category +"metrics", then `install_skill`), and follow it end-to-end — it owns where +metrics belong (request middleware, background +jobs, external calls, business commit sites), which of counter, gauge, and +histogram fits each site, and the low-cardinality attribute rules. + +Import the client the way the verify handoff names it. Only additive changes: +add metric calls, never restructure the code they land in. + +## How you know you succeeded + +The hot paths and business commit sites record metrics through the one shared +client, the project still builds, and your handoff lists every metric name +with its type and where it fires. diff --git a/src/programs/metrics/agent/report.md b/src/programs/metrics/agent/report.md new file mode 100644 index 000000000..d1b5362cf --- /dev/null +++ b/src/programs/metrics/agent/report.md @@ -0,0 +1,44 @@ +--- +type: report +flow: metrics +label: Report and hand off +sink: true +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-notebook] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [enqueue_task] +dependsOn: [instrument-metrics] +--- + +## Goal + +Tell the user what their service now measures, from the handoffs of every task +in the run. Briefly and concretely: + +- Each metric now recording: its name, type (counter, gauge, histogram), and + where in the code it fires. +- The service name the client reports under, and the SDK version the run + verified or installed. +- Anything skipped or deferred, with the reason — an environment that blocked + an upgrade is an outcome, not an apology. +- Where results appear: the Metrics page in PostHog, with the first series + arriving as traffic flows. + +If the run also installed or initialized the SDK, say so — the user started +this command without PostHog and now has it. + +Publish the report with a single `publish_handoff` call. The same call mirrors +the markdown into a shareable PostHog notebook. The notebook skill says what the result means and covers +older wizards. Do not write a report file. The handoff call and the notebook +are how the report reaches the user. + +## How you know you succeeded + +A user who reads only your report knows what their service measures, where +each number comes from, what still needs them, and where to look first. The +report went through one `publish_handoff` call. The notebook mirror is best +effort: when it was created, the user has its link, and when it wasn't, your +final message says why, and when an update failed, that the notebook holds the earlier version. A missing notebook never fails this task. diff --git a/src/programs/metrics/agent/setup-metrics.md b/src/programs/metrics/agent/setup-metrics.md new file mode 100644 index 000000000..de6259ae2 --- /dev/null +++ b/src/programs/metrics/agent/setup-metrics.md @@ -0,0 +1,60 @@ +--- +type: setup-metrics +flow: metrics +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Glob, Grep] +disallowedTools: [Write, Edit, Bash, complete_task] +dependsOn: [] +--- + +## Goal + +Plan an application-metrics setup and seed the task queue. Metrics are a +per-service concern: every service gets its own SDK install and its own +`service_name`. + +First map the repo — mechanically, before any judgment. Run one `find` per +manifest pattern across the whole tree, every time, even when the root looks +like a plain npm workspace (repos mix ecosystems, and the workspace list in a +root manifest is not the whole map): + + **/package.json **/pyproject.toml **/requirements.txt **/Pipfile + **/go.mod **/composer.json **/Gemfile + +Every directory holding a hit is a package. Classify each: a **service** runs +server-side work (server entrypoints, workers, APIs — whatever the language); +browser-only apps and libraries are not services and get no chain (the report +notes them). Ignore installed-dependency and scaffolding directories +(`node_modules`, `.claude`, caches) — they are not packages. + +Then seed, per service: + +- `verify-sdk`, with no dependencies — it settles that service's SDK question + (installed, new enough, initialized for metrics) before any + instrumentation. Pass `inputs: { package: "", service_name: "" }` + so the task knows which package it owns; omit `package` in a single-package + repo. +- `instrument-metrics`, after that service's `verify-sdk`, with the same + inputs. + +Chains for different services are independent — do not wire edges between +them, so they run in parallel. Last, one `report` that depends on every +`instrument-metrics`. It writes the handoff last, so it describes what +actually shipped. + +Metrics ride each service's existing PostHog SDK (`posthog.metrics`), so the +graph is the same whether or not the repo has PostHog today — `verify-sdk` +absorbs the difference. Never plan an identify, capture, or dashboard task: +this run sets up application metrics, not the full integration. + +## How you know you succeeded + +Every service has its own verify → instrument chain carrying its package and +service name, the chains share no edges, the report depends on all of them, +and every `verify-sdk` is runnable. Keep labels short — the action in a few +words, naming the service when there are several. diff --git a/src/programs/metrics/agent/verify-sdk.md b/src/programs/metrics/agent/verify-sdk.md new file mode 100644 index 000000000..a17445f0a --- /dev/null +++ b/src/programs/metrics/agent/verify-sdk.md @@ -0,0 +1,46 @@ +--- +type: verify-sdk +flow: metrics +label: Verify the PostHog SDK is ready for metrics +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Edit, Glob, Grep, Bash, load_skill_menu, install_skill] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Leave your service with a PostHog SDK that can record metrics. When your +task input names a `package`, that package is your whole world — its +manifest, its init site, its `service_name`; do not touch sibling packages. +First call +`load_skill_menu` with `category: "metrics"` and `install_skill` for the +variant matching the project's platform — the menu is the source of truth, +one variant per platform, and a full-stack app takes the server variant +because metrics measure service work. The installed skill carries the +platform's install and init specifics. Then three checks, in order: + +1. **Installed?** Look for the SDK in the dependency manifest. Missing → + install it with the project's own package manager and let it resolve the + version — never invent one. +2. **New enough?** `posthog.metrics` ships in recent SDK releases only — the + skill states the version floor for this platform. Below it → bump with the + package manager's upgrade command. At or above → leave it alone and say so + in your handoff. +3. **Initialized for metrics?** Find where the client is constructed. Add the + `metrics` config (a service name) to that existing client, as the skill + shows. No client anywhere → initialize one, following the skill. Never + construct a second client beside a working one, and never touch identify + calls, event capture, or other init options. + +## How you know you succeeded + +The manifest holds the SDK at a metrics-capable version and exactly one client +init carries the metrics config — or your handoff plainly says why the +environment stopped you. Name the skill variant you installed, the manifest, the version, and the +init site in your handoff, so instrumentation installs the same skill and +knows what to import and where. diff --git a/src/programs/metrics/flow.ts b/src/programs/metrics/flow.ts new file mode 100644 index 000000000..e3dad6ca9 --- /dev/null +++ b/src/programs/metrics/flow.ts @@ -0,0 +1,15 @@ +import { defineFlow } from '@programs/program-contract'; + +/** One verify → instrument chain per service, which the planner fans out; the report waits for all of them. */ +export const flow = defineFlow({ + sink: 'report', + nodes: { + 'verify-sdk': { picked: true }, + 'instrument-metrics': { + picked: true, + dependsOn: ['verify-sdk'], + matchInputs: ['package', 'service_name'], + }, + report: { dependsOn: ['instrument-metrics'] }, + }, +}); diff --git a/src/programs/metrics/index.ts b/src/programs/metrics/index.ts index a8d16ed94..9230ca3c2 100644 --- a/src/programs/metrics/index.ts +++ b/src/programs/metrics/index.ts @@ -1,5 +1,6 @@ import { Harness, Sequence, DEFAULT_AGENT_MODEL } from '@shared/constants'; import type { ProgramConfig } from '@programs/program-contract'; +import { flow } from './flow'; const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; @@ -30,6 +31,8 @@ export const config: ProgramConfig = { // platform variant themselves. Explicit so renaming the program can't // silently detach the flow. agentFlow: 'metrics', + agentPrompts: () => import('./agent-prompts').then((m) => m.AGENT_PROMPTS), + flow, handoffNotebook: true, reportFile: METRICS_REPORT_FILE, run: { diff --git a/src/programs/posthog-integration/agent-prompts.ts b/src/programs/posthog-integration/agent-prompts.ts new file mode 100644 index 000000000..97d96fcb2 --- /dev/null +++ b/src/programs/posthog-integration/agent-prompts.ts @@ -0,0 +1,27 @@ +// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. + +/** This program's agent prompts by task type, verbatim. */ +export const AGENT_PROMPTS: Readonly> = { + 'ai-observability': + "---\ntype: ai-observability\nflow: integration-v2\nlabel: Add AI Observability\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: high\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture]\noptional: true\n---\n\n## Goal\n\nAdd AI Observability to the LLM calls this project already makes. Read the\nupstream handoffs and inspect the call sites, not just the dependency names.\nIf there are no LLM calls, complete as `not needed` with the reason and make no\nchanges. Do not install a vendor SDK, add a demo call, or use manual capture to\ninvent an AI feature in an app that has none.\n\nFor existing LLM calls, load the `ai-observability` skill menu and install the\nvariant matching the calling code's language and provider or agent framework.\nFollow that skill's selection rules and instrumentation references. Manual\ncapture is appropriate only for existing calls without a supported wrapper.\nInstrument against the provider SDK version the app already uses; never\nrequire, assume, or request an upgrade of the app's own dependencies — if the\nwrapper cannot support the app's version, fall back to manual capture or\nreport why. Reuse the PostHog client and identity established earlier in this\nrun. Follow the skill's privacy-mode instructions: default new SDK clients to\n`privacy_mode=False` (Python) or `privacyMode: false` (Node), so prompt and\ncompletion content is captured. Preserve existing privacy settings, redaction,\nand explicit user or project requirements. Use the variant's documented controls\nwhere capture is configured elsewhere; do not strip manual-capture content by\ndefault. If a variant cannot be chosen from the code, report the ambiguity as\n`not needed`; this default run does not ask the user to choose a provider.\n\nThis is the instrumentation part of a larger integration. Name the packages\nthis task needs in your handoff for review to install; do not edit dependency\nmanifests, and never write a version number you invented — the package manager\nresolves real versions when review installs by name. Defer the skill's\npackage-manager and import-check commands to that task. For Go, review adds the\nmodules with `go get` and `go mod tidy`; do not hand-edit `go.mod` or `go.sum`.\nDo not run the app or make paid LLM calls. Use `check_env_keys` and\n`set_env_values` for environment files, using the project credentials supplied\nby the wizard.\n\n## How you know you succeeded\n\nExisting LLM calls are wired using the selected skill, or the handoff clearly\nexplains why this task was not needed. List the variant, changed files, declared\ndependencies, and a concrete call path the user can trigger to verify the\nsession/trace/generation tree. Include the skill's **Privacy mode** handoff:\nthe effective setting, the file and line to edit, when to enable it, and the\n[privacy-mode docs](https://posthog.com/docs/ai-observability/privacy-mode). Pass any\ndeferred dependency work to review.\nDescribe delivery as unverified unless this run actually observed it; do not\npublish a separate setup report.\n", + capture: + "---\ntype: capture\nflow: integration-v2\nlabel: Capture events\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-capture, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init, identify]\n---\n\n## Goal\n\nDecide which events are worth capturing in this app, then instrument them in the\nsame pass — read each file once, choose the events, and add the capture calls\nwhile the file is already open. Instrumenting the events is the job; everything\nbelow is a rule you apply while you do it, not work of its own.\n\nThe identify step's handoff already says how this app attributes events. Read it\nonce, up front, and follow it:\n\n1. Where identity is established for you — a client library that remembers it, or a\n framework middleware that binds it per request — a plain capture is already\n attributed. Write the plain call and move on. Do not add a context of your own,\n and do not go looking through the call tree to prove it.\n2. Where the handoff says it is not — no middleware, or a call path that runs\n outside the request — tag that call with the distinct id and the session id.\n\nWhere the project has no identity to begin with — the identify step found nothing to\nwire, because the app has no accounts or login — personless events are the right\nanswer. Capture plainly, with no id and no placeholder, and say so in your handoff.\n\nThe placeholder is for the third case only: identity exists in this project, but no\nstable id reaches the call site. Do not guess at the id and do not invent a fallback —\npass `DISTINCT_ID` there so the gap is visible in the code, and say in your handoff\nthat identity exists but could not be reached, so the report calls it out as an issue\nto follow up rather than claiming events that belong to nobody.\n\nNever put PII in an event. When new information about the user surfaces, tag the\nuser the way the identify docs describe, not the event.\n\n## How you know you succeeded\n\nThe meaningful user actions across the app have capture calls that fire on the\nreal action, not on page load, each one attributable to the user who took it, and\n`.posthog-wizard-cache/.posthog-events.json` lists the events you instrumented.\n\nYou do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.\n", + dashboard: + '---\ntype: dashboard\nflow: integration-v2\nlabel: Create a starter dashboard\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-dashboard, integration-v2-insight, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [capture]\n---\n\n## Goal\n\nCreate a starter PostHog dashboard with a few insights built on the events this\nintegration instruments, using the PostHog MCP.\n\n## How you know you succeeded\n\nA dashboard exists with a handful of insights on the captured events, and you hand\noff its URL for the report to link.\n', + 'error-tracking': + "---\ntype: error-tracking\nflow: integration-v2\nlabel: Add error tracking\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-error-tracking-step, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nMake the errors the app does not catch reach PostHog, by whatever means the SDK\noffers for that. Which means depends on the SDK: some autocapture exceptions once\nyou enable it at init, some wire into the framework's own error handler, some give\nyou a boundary to mount at the app entry. Follow the docs and the reference example\nfor this one, and set it up in one place — never manual capture calls sprinkled\nacross files.\n\nThe SDK is already installed and initialized (see the context from previous steps);\nbuild on that, do not re-check it.\n\nThis is an instrument-only task. Do not install dependencies, run the build, run\ntests, or start the app — a later `build` step does all verification. Stay inside\nthis project's directory and set up that one place; that is the whole job.\n\n## How you know you succeeded\n\nAn error the app does not catch reaches PostHog, through the mechanism this SDK\ngives you rather than one you invented. You did not install anything, run a build,\nlint, or tests (the review task verifies after you), search outside the project, or read through the whole app or hand-wrap\nindividual components or routes.\n", + identify: + "---\ntype: identify\nflow: integration-v2\nlabel: Wire user identification\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-identify, posthog-best-practices]\nallowedTools: [Read, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nWire user identification so everything the app reports — its errors as much as its\nevents — carries who the user is.\n\nThe distinct id is the app's own stable identifier for the user — a primary key, a\nuuid, a resource name. Not an email and not a username: those are PII and they change;\nsend them as person properties instead. Fall back to email only when the app truly has\nno other stable key, and say so in your handoff — do not invent one by altering the\napp's schema.\n\nWork out first what you are in: a client-only app, a fullstack or SSR app, or a\nbackend or API. Identification takes a different shape in each, and the shape\nfollows from that answer:\n\n1. Client and frontend — identify once, and only once, when the user logs in or\n registers, and reset when they log out. That single call merges what was already\n captured against the anonymous id into the stable distinct id you give it. The\n library persists identity from there, so everything after it is attributed to the\n same user until reset — nothing needs re-identifying per page or per event.\n2. Fullstack or SSR — your identification hinges on the client side, where the\n identity is persisted. The distinct id and the session id travel on every request\n the frontend sends, in the header format the docs describe. Some libraries already\n do this for you; read the docs and the example app before wiring it by hand. The\n backend should take note of the session and distinct id as soon as possible and\n either carry it to every capture or use the identified context to identify the\n request.\n3. Backend and API — a server serves many users at once, so identity belongs to the\n request, never to the process. Take it from the incoming request or the ambient\n context, as far as the app allows. Then either tag each event with the distinct\n id and session id, or, where the SDK has contexts, open one around the whole\n request and identify that context so everything the request reports inherits it.\n Where the SDK ships a middleware for this framework, that is what does this — use\n it rather than writing your own. Read the docs and the example app.\n\n## How you know you succeeded\n\nIdentification matches the kind of app you are in. While a user is known, what the\napp reports is attributable to them with a stable distinct id — the errors it\nreports, not only the events — established at the boundary the SDK gives you rather\nthan repeated at call sites that already inherit it. Where the app changes who the\nuser is mid-request, that boundary cannot know it yet, and establishing identity\nagain there is the point, not a duplicate. Your handoff names the files you\nchanged, how identity is established in them, and what a later step must do for its\nown calls to inherit it — whether that is nothing at all, or tagging each call\nitself. If the app has no auth or user concept, say so and stop.\n\nYou do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.\n", + init: "---\ntype: init\nflow: integration-v2\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nInitialize PostHog: create the framework's init point so the SDK is configured\nonce and available across the app, set the PostHog environment variables through\nthe wizard tools, and document those keys in `.env.example` for other developers.\n\nTry to follow these principles, in order: codebase convention, don't make any\nunnecessary changes, keep the change as lean as possible for an easy review, and\nfollow the shape of the examples provided. How call sites reach the client is\npart of the shape: reach it the way the docs and the example project reach it.\n\n## How you know you succeeded\n\nThe init file exists and the PostHog env keys are present. Keys live in the env\nfile, never hardcoded in source, and `.env.example` lists the key names (with\nplaceholder values) so the next developer knows what to set. Your handoff names the\nfiles you changed, how the client is constructed in them, and how a call site\nreaches that client — every step after you has to reach the same one the same way.\nIf the app ships a Content-Security-Policy, the handoff also names exactly\nwhich directives you touched (`script-src`, `connect-src`, `worker-src`) and\nwhich you left alone — the review verifies directive-by-directive, and an\nunnamed directive reads as unhandled, not as fine.\n\nYou do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.\n", + install: + "---\ntype: install\nflow: integration-v2\nlabel: Add the PostHog SDK to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-haiku-4-5\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nInstall the PostHog SDK with the project's own package manager, so the real\npublished version lands in the manifest and the lockfile — not one you made up.\nRun the manager's add command with the bare package name and let it resolve the\nversion: `npm install ` (or the project's pnpm/yarn/bun), `pip install `,\n`composer require `, `bundle add `, `gem install `. Never invent a\nversion — one that was never published fails the whole run with `ETARGET`, and\nguessing another just fails again. The only version you write by hand is one copied\nfrom the framework reference example, and only in the environment fallback below.\n\nAdd the client library, plus the server library if the app has server-side code\nthat sends events. If the SDK is already installed, leave it and say so.\n\nSome ecosystems pin an explicit version in a manifest no install command resolves\n(Swift SPM, Gradle). There, declare the dependency the way the framework reference\nshows and take the version from the SDK's latest release — do not invent one.\n\nAttempt the install once. If it fails on something you own — a wrong package name or\nthe wrong manager — fix that and run it one more time.\n\nBut if the environment prevents the install through no fault of your change, do NOT\nspiral retrying it. These are environment failures, not yours:\n\n- a peer-dependency conflict the project already had (npm `ERESOLVE`),\n- a dependency already in the manifest that itself does not resolve (a bad or yanked\n version, a package that no longer exists) — the failure names a package that is not\n PostHog,\n- the package manager crashing on the project's existing dependency graph or a\n corrupt/incompatible lockfile (e.g. npm `Cannot read properties of null`),\n- the package manager itself failing to run or provision — corepack unable to\n switch to a pinned version (`Failed to switch pnpm to …`, `Cannot find matching\n keyid`), a missing manager binary, a toolchain the machine does not have,\n- no network or registry reachable.\n\nThe test: if the failure names your PostHog package or your command, it is yours to\nfix; if it names another dependency, a version pin the project set, or the toolchain\nitself, it is the environment — fall back and move on.\n\nWhen you hit one of those, fall back: add the PostHog package to the manifest by hand\nat a known-good version, so the dependency is still declared for the later steps and\nfor the user's own install. Use the framework reference example's spec if it names\none; otherwise use a caret on the SDK's current known-good release — `posthog-js`\n`^1.407.2`, `posthog-node` `^5.46.1`, `posthog-php` `^4.12.1`, `posthog` (Python)\n`^7.29.0`, `posthog-ruby` `^3.21.0`. The caret keeps it current without pinning a\npatch; never invent a version that was never published. Then move on, and say in your\nhandoff exactly what failed and the command\nyou ran, so the review step and the report surface it.\n\n## How you know you succeeded\n\nThe SDK resolved and installed at its real version, or you have reported plainly in\nyour handoff why the environment stopped it. Your handoff names the manifest and the\npackage, so later steps import it under the name they will actually get.\n", + 'integrate-posthog': + "---\ntype: integrate-posthog\nflow: integration-v2\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan a PostHog integration and seed the task queue with this graph:\n\n- `install` and `init`, independent of each other.\n- `identify` and `error-tracking`, each after `install` and `init` and independent\n of one another, so they run in parallel. `error-tracking` makes the errors the app\n does not catch reach PostHog, by whatever means the SDK offers for that — it needs\n the SDK installed and initialized, not the events.\n- `capture`, after `identify` — it decides the events and instruments them, and it\n reads how identity is already established before it instruments anything.\n- `ai-observability`, after `capture`, parallel to `error-tracking` — it instruments\n existing LLM calls, or reports that none apply. Queue it on every default run; the\n task checks applicability rather than the planner guessing from a package name.\n- `logs`, after `ai-observability` — it configures log capture on supported\n platforms, or reports why it does not apply. Queue it even when AIO will be\n skipped. These tasks run sequentially because both instrument the same entry\n and initialization files, and Logs preserves any tracing provider AIO\n configures — an ordering-dependent instruction. (Their dependency installs\n are deferred to review, so manifests are not the conflict.)\n- `review`, after `install`, `init`, `identify`, `capture`, `error-tracking`,\n `ai-observability`, and `logs` —\n it installs the dependencies, verifies the project builds/typechecks/lints, and\n reviews every change the run made, fixing what fails. There is no separate build\n step: verifying and reviewing are one pass over the same changeset.\n- `dashboard`, after `capture`, independent of AIO, Logs, and `review` — it builds insights from the\n instrumented events, which `capture` has already defined; it needs no code review.\n- `report`, after `dashboard` **and** `review` — it writes the setup report last, so\n it describes the integration as reviewed rather than as first written.\n\nIf `enqueue_task` does not offer a task's type, this run excludes that product\n— skip it rather than retrying, and hang nothing off it.\n\n## How you know you succeeded\n\nEvery task in the graph is queued with that dependency shape, the report last,\nand the first task runnable. Keep labels short — the action in a few words.\n", + logs: "---\ntype: logs\nflow: integration-v2\nlabel: Add log capture\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [ai-observability]\noptional: true\n---\n\n## Goal\n\nConfigure PostHog log capture for this project's supported runtimes. Load the\n`logs` skill menu, install its matching skill, and read the platform reference\nbefore editing. Inspect the runtime and existing logging setup; use the\ndocumented OTLP exporter or SDK-native log capture for that platform. If the\napp already uses OpenTelemetry, write against its existing OTel version and\nname exporter packages compatible with it — never require, or write code that\nassumes, an upgrade of a package this integration did not introduce. A\nbrowser-only app must not receive a server exporter. If no documented setup\napplies, complete as `not needed` and explain why.\n\nExport only log lines this integration adds. Create a dedicated logger for the\nPostHog exporter and emit a few purpose-written lines at meaningful points the\napp already passes through. Even where the platform reference attaches the\nexporter to the application's root or existing loggers, do not — the app's\nexisting logs are data this run has no mandate to export, and every line that\nleaves must be visible in this run's diff. Reuse any existing PostHog log\nexport and leave all existing handlers and outputs intact; if capture is\nalready configured, report that without adding a second exporter. Preserve any\ntracing provider AIO configured in the previous step. Do not export secrets,\nrequest bodies, or user data, and do not scatter logs through unrelated code.\n\nThis is the instrumentation part of a larger integration. Instead of the\nstandalone skill's install command, name the required packages in your handoff\nand leave installation, manifest and lockfile updates, builds, and lint to\nreview. Do not edit dependency manifests or write a version number you\ninvented — the package manager resolves real versions when review installs by\nname. Use `detect_package_manager` if needed. Use\nthe wizard's supplied project and region and the env names from upstream\nhandoffs; inspect and write environment files only through `check_env_keys`\nand `set_env_values`. Do not request credentials or select a different project.\nIf required configuration is unavailable, report what is missing instead of\nguessing a region.\n\n## How you know you succeeded\n\nLog capture is configured once for the supported runtime, was already present,\nor has a clear skip reason. The handoff names the changed files, packages, env\nvariable names (never values), and any dependency work for review. Give a\nspecific code path the user can trigger to produce one of the added log lines\nand find it in PostHog Logs; delivery remains unverified until observed. Note\nfor the report that only lines added by this run are exported, and the one-line\nchange that routes the app's existing loggers into the same exporter if the\nuser wants more. Do not publish a separate report.\n", + report: + "---\ntype: report\nflow: integration-v2\nlabel: Write the setup report\nsink: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-report, integration-v2-notebook, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: [dashboard, review]\n---\n\n## Goal\n\nCompose the setup report summarizing what this integration did, drawing only on\nwhat the run itself recorded: the queue log and event plan in\n`.posthog-wizard-cache/` (`queue.json` and `.posthog-events.json`), and the\nhandoff each step left behind. Then publish it with a single `publish_handoff`\ncall. The same call mirrors the markdown into a shareable PostHog notebook. The notebook skill says what the result\nmeans and covers older wizards. Do not write a report file. The handoff call\nand the notebook are how the report reaches the user.\n\nSeparate what the run verified from what it did not. A passing build proves the\ncode compiles, not that events flow — never write that an event was captured\nunless the run observed it arrive. Where a step failed or was skipped, say so\nplainly instead of rounding it up to success. Anything only the user can confirm\nbelongs in a checklist to work through before merging, each item naming the file\nand line to look at.\n\nWhere a handoff carries a report section, that step already wrote the part of\nthe report it owns. Include it as its own section instead of rewriting or\nsummarizing it, and do not restate its contents elsewhere.\n\nWhere a handoff reports something a step could not resolve — attribution it could not\nestablish, a question the step before it left open — that is not a caveat to bury in\nprose. Raise it as its own issue to follow up, saying what is unresolved and what it\ncosts if left alone. A `DISTINCT_ID` placeholder left at a call site means no stable id\nwas available: name every file and line carrying one, so the user knows what to replace\nbefore those events mean anything.\n\n## How you know you succeeded\n\nOne `publish_handoff` call went through with the full report: what was\ninstalled and initialized, the events captured, whether identify was wired or\nskipped, error tracking added, AI Observability and Logs configured, skipped, or\nfailed with reasons, the dashboard link, any build conflict in full,\nand the next steps for the user. Every claim in it traces to a handoff, and\nwhat the run could not confirm reads as unconfirmed. The notebook mirror is\nbest effort: when it was created, the user has its link, and when it wasn't,\nyour final message says why, and when an update failed, that the notebook holds the earlier version. A missing notebook never fails this task.\n", + review: + "---\ntype: review\nflow: integration-v2\nlabel: Verify and review the integration\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [posthog-best-practices, integration-v2-build]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init, identify, error-tracking, capture, ai-observability, logs]\n---\n\n## Goal\n\nYou are the last hands on the code. First make the integration run, then review it,\nthen leave it building to the best of your ability. Do both in one pass, verifying and reviewing are the same\nreading of the same changeset.\n\n**Install and verify.** The earlier steps edited code but did not install —\ninstall now, then verify the project builds, typechecks, and lints, whichever\nof those it defines. Install what the manifest declares plus every package the\nupstream handoffs name, adding the named ones by bare name with the project's\npackage manager so it resolves real versions. The app's existing dependencies\nare not yours to touch: never upgrade, downgrade, or re-add a package this\nintegration did not introduce. If an instrumentation package's peer range\nconflicts with a dependency the app already has, install an older\ninstrumentation version that fits the app's existing versions — find it with\nthe package manager, stepping down major versions (`pkg@^7`, then `@^6`,\n…) for a few attempts rather than looking for documentation of one. If none\nfits, revert that piece of instrumentation, leave the app's dependency alone,\nand record the incompatibility so the report can say what was skipped and why. An optional upstream task\n(AI Observability, Logs) may have failed and left partial edits no handoff\ndescribes — install and build either way, and let the build surface stray\nimports; fix or revert them like any other defect. Do not run the test suite; the\nruntime does not allow it, and a green build is the bar. A bad version only surfaces\nhere: if the install fails because a declared version does not exist (npm `ETARGET`,\n\"no matching version\", a yanked release), do not guess pins — reinstall the\npackage by bare name and let the package manager pick, matching the framework\nreference example's spec only if that also fails. If the failure is the environment, not the\nintegration's own change (a pre-existing broken dependency, a package manager that\ncannot provision), note it and move on; do not spiral.\n\n**Review, as a strict reviewer who did none of the writing.** The changeset is\nalready handed to you: `read_handoffs` returns every upstream task's handoff, and\neach one names the files it touched and what it changed in them. That union of\ntouched files IS the changeset. Read each changeset file once and judge it — after\nyour own edit you already know its contents, so do not re-read it. Read beyond the\nchangeset only where a dimension sends you: one neighboring example per pattern you\nare checking, and the callers of code that changed. Nothing else — not the SDK's\ninstalled internals, not other tasks' instructions, not a diff of the whole tree;\nthe handoffs are the signal.\nThe handoffs also carry what no file shows — env values written through\n`set_env_values`, manifest edits. Judge each change against these dimensions, in\norder:\n\n1. Minimal — every line earns its place. Delete helpers nothing calls, defensive\n wrapping around code that cannot fail, config for features that were not set up,\n and comments that narrate what a line does or that it was added.\n2. Unrelated — the run must not have changed behavior outside the integration.\n Revert reformatting, renames, or edits in code the integration never needed.\n3. Codebase patterns — for each kind of edit (client construction, how call sites\n reach it, config and env access, imports, naming, comment density), find one\n neighboring example in this project and compare. If a change invented a seam the\n project does not have — a registry, locator, wrapper, or app the codebase never\n uses — replace it with the closest pattern the project actually uses.\n4. Example shapes — where the framework reference example shows a shape for init or\n instrumentation, the code should be recognizably that shape adapted to this\n codebase. Codebase idiom beats the example when they conflict; the PostHog\n correctness rules you were given beat both.\n\nYou never change what is captured — event names, properties, and where events fire\nare a contract the dashboard and report are built on. Refactor how, and kill bad\nchanges; never alter the what.\n\nEvents reaching PostHog in production is the point of this code, so a capture wired\nto a place it will never fire, an uninitialized SDK, or a call the runtime silently\ndrops is squarely yours to catch and fix — it is a code defect. What you cannot do is\nconfirm delivery: you cannot exercise the app, and you never treat \"I could not\nverify events arrive\" as a finding.\n\nStay inside verify-and-review. These are not your job:\n\n- The report or the dashboard — other tasks own those; do not write or judge them.\n- Product or analytics design — which events are worth tracking, identity strategy,\n framework-version tradeoffs. The changeset is the decision; review whether the code\n implements it correctly, not whether it was the right decision.\n- Anything the integration did not touch. Do not widen scope to pre-existing issues.\n\nIf you notice something real but out of scope, leave it — a one-line mention in your\nhandoff, no more. Only flag what you can pin to a specific line and dimension — no\ntaste-based rewrites, and never expand the integration. For each fix, ask what would\nbehave differently if it were wrong, and check by reading the callers. After any\nchange, re-run the build or lint and confirm it still passes.\n\n## How you know you succeeded\n\nThe project installed and builds, typechecks, and lints as well as it did before the\nrun; you can name each review dimension and say it passes; and your fixes are in the\nfiles. If the build or lint fails only on pre-existing errors you did not introduce,\nthat still counts — put a one-line summary in the `conflict` field and the detail in\nwhat you did. Reserve a failed status for when your own changes break the build. Put\nthe number of fixes and the dimension each addressed in your handoff.\n", + warehouse: + "---\ntype: warehouse\nflow: integration-v2\nlabel: Connect your data sources\nrunnerSeeded: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-warehouse, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, wizard_ask, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nConnect the data sources listed in your task input to PostHog's data warehouse,\nso the project's own data can be queried next to the events this run\ninstruments. The wizard found them in the project; your job is to turn each one\ninto a real connection, or into a link the user can finish in the browser.\n\nYou are the only step that stops to ask the user for anything, so make the ask\ncount: send the user to that source's secure connect page in one `wizard_ask`\ncall, tagged with that source's kind as its `subject`. The user enters the\ncredentials on that page, so no secret passes through you. A decline answers\nthat one source. Move on to the next source rather than re-asking or ending the\nround.\n\nTouch no project code. This step connects data; it does not edit the app.\n\n## How you know you succeeded\n\nEvery source in your input reached a decided end: connected in PostHog, handed\nback as a link to finish, or skipped with a reason. Your handoff carries a\nfinished report section naming each source and which of those three it was, so\nthe reporting step can include it as it stands.\n\nA link is a handoff, not a connection. Your report section opens with the count\nyou connected, and your `complete_task` status matches it: `done` only when you\nconnected at least one source.\n", +}; diff --git a/src/programs/posthog-integration/agent/ai-observability.md b/src/programs/posthog-integration/agent/ai-observability.md new file mode 100644 index 000000000..942e05f90 --- /dev/null +++ b/src/programs/posthog-integration/agent/ai-observability.md @@ -0,0 +1,60 @@ +--- +type: ai-observability +flow: integration-v2 +label: Add AI Observability +model_pi: openai/gpt-5.6-terra +effort_pi: high +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill] +disallowedTools: [enqueue_task] +dependsOn: [capture] +optional: true +--- + +## Goal + +Add AI Observability to the LLM calls this project already makes. Read the +upstream handoffs and inspect the call sites, not just the dependency names. +If there are no LLM calls, complete as `not needed` with the reason and make no +changes. Do not install a vendor SDK, add a demo call, or use manual capture to +invent an AI feature in an app that has none. + +For existing LLM calls, load the `ai-observability` skill menu and install the +variant matching the calling code's language and provider or agent framework. +Follow that skill's selection rules and instrumentation references. Manual +capture is appropriate only for existing calls without a supported wrapper. +Instrument against the provider SDK version the app already uses; never +require, assume, or request an upgrade of the app's own dependencies — if the +wrapper cannot support the app's version, fall back to manual capture or +report why. Reuse the PostHog client and identity established earlier in this +run. Follow the skill's privacy-mode instructions: default new SDK clients to +`privacy_mode=False` (Python) or `privacyMode: false` (Node), so prompt and +completion content is captured. Preserve existing privacy settings, redaction, +and explicit user or project requirements. Use the variant's documented controls +where capture is configured elsewhere; do not strip manual-capture content by +default. If a variant cannot be chosen from the code, report the ambiguity as +`not needed`; this default run does not ask the user to choose a provider. + +This is the instrumentation part of a larger integration. Name the packages +this task needs in your handoff for review to install; do not edit dependency +manifests, and never write a version number you invented — the package manager +resolves real versions when review installs by name. Defer the skill's +package-manager and import-check commands to that task. For Go, review adds the +modules with `go get` and `go mod tidy`; do not hand-edit `go.mod` or `go.sum`. +Do not run the app or make paid LLM calls. Use `check_env_keys` and +`set_env_values` for environment files, using the project credentials supplied +by the wizard. + +## How you know you succeeded + +Existing LLM calls are wired using the selected skill, or the handoff clearly +explains why this task was not needed. List the variant, changed files, declared +dependencies, and a concrete call path the user can trigger to verify the +session/trace/generation tree. Include the skill's **Privacy mode** handoff: +the effective setting, the file and line to edit, when to enable it, and the +[privacy-mode docs](https://posthog.com/docs/ai-observability/privacy-mode). Pass any +deferred dependency work to review. +Describe delivery as unverified unless this run actually observed it; do not +publish a separate setup report. diff --git a/src/programs/posthog-integration/agent/capture.md b/src/programs/posthog-integration/agent/capture.md new file mode 100644 index 000000000..e77450f40 --- /dev/null +++ b/src/programs/posthog-integration/agent/capture.md @@ -0,0 +1,51 @@ +--- +type: capture +flow: integration-v2 +label: Capture events +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-capture, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init, identify] +--- + +## 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. Instrumenting the events is the job; everything +below is a rule you apply while you do it, not work of its own. + +The identify step's handoff already says how this app attributes events. Read it +once, up front, and follow it: + +1. Where identity is established for you — a client library that remembers it, or a + framework middleware that binds it per request — a plain capture is already + attributed. Write the plain call and move on. Do not add a context of your own, + and do not go looking through the call tree to prove it. +2. Where the handoff says it is not — no middleware, or a call path that runs + outside the request — tag that call with the distinct id and the session id. + +Where the project has no identity to begin with — the identify step found nothing to +wire, because the app has no accounts or login — personless events are the right +answer. Capture plainly, with no id and no placeholder, and say so in your handoff. + +The placeholder is for the third case only: identity exists in this project, but no +stable id reaches the call site. Do not guess at the id and do not invent a fallback — +pass `DISTINCT_ID` there so the gap is visible in the code, and say in your handoff +that identity exists but could not be reached, so the report calls it out as an issue +to follow up rather than claiming events that belong to nobody. + +Never put PII in an event. When new information about the user surfaces, tag the +user the way the identify docs describe, not the event. + +## 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, each one attributable to the user who took it, and +`.posthog-wizard-cache/.posthog-events.json` lists the events you instrumented. + +You do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading. diff --git a/src/programs/posthog-integration/agent/dashboard.md b/src/programs/posthog-integration/agent/dashboard.md new file mode 100644 index 000000000..349e49c47 --- /dev/null +++ b/src/programs/posthog-integration/agent/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: low +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-dashboard, integration-v2-insight, integration-v2-mcp] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [capture] +--- + +## 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/src/programs/posthog-integration/agent/error-tracking.md b/src/programs/posthog-integration/agent/error-tracking.md new file mode 100644 index 000000000..ee76c1382 --- /dev/null +++ b/src/programs/posthog-integration/agent/error-tracking.md @@ -0,0 +1,36 @@ +--- +type: error-tracking +flow: integration-v2 +label: Add error tracking +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-error-tracking-step, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Make the errors the app does not catch reach PostHog, by whatever means the SDK +offers for that. Which means depends on the SDK: some autocapture exceptions once +you enable it at init, some wire into the framework's own error handler, some give +you a boundary to mount at the app entry. Follow the docs and the reference example +for this one, and set it up in one place — never 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 set up that one place; that is the whole job. + +## How you know you succeeded + +An error the app does not catch reaches PostHog, through the mechanism this SDK +gives you rather than one you invented. You did not install anything, run a build, +lint, or tests (the review task verifies after you), search outside the project, or read through the whole app or hand-wrap +individual components or routes. diff --git a/src/programs/posthog-integration/agent/identify.md b/src/programs/posthog-integration/agent/identify.md new file mode 100644 index 000000000..9c2e0cbf1 --- /dev/null +++ b/src/programs/posthog-integration/agent/identify.md @@ -0,0 +1,62 @@ +--- +type: identify +flow: integration-v2 +label: Wire user identification +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-identify, posthog-best-practices] +allowedTools: [Read, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Wire user identification so everything the app reports — its errors as much as its +events — carries who the user is. + +The distinct id is the app's own stable identifier for the user — a primary key, a +uuid, a resource name. Not an email and not a username: those are PII and they change; +send them as person properties instead. Fall back to email only when the app truly has +no other stable key, and say so in your handoff — do not invent one by altering the +app's schema. + +Work out first what you are in: a client-only app, a fullstack or SSR app, or a +backend or API. Identification takes a different shape in each, and the shape +follows from that answer: + +1. Client and frontend — identify once, and only once, when the user logs in or + registers, and reset when they log out. That single call merges what was already + captured against the anonymous id into the stable distinct id you give it. The + library persists identity from there, so everything after it is attributed to the + same user until reset — nothing needs re-identifying per page or per event. +2. Fullstack or SSR — your identification hinges on the client side, where the + identity is persisted. The distinct id and the session id travel on every request + the frontend sends, in the header format the docs describe. Some libraries already + do this for you; read the docs and the example app before wiring it by hand. The + backend should take note of the session and distinct id as soon as possible and + either carry it to every capture or use the identified context to identify the + request. +3. Backend and API — a server serves many users at once, so identity belongs to the + request, never to the process. Take it from the incoming request or the ambient + context, as far as the app allows. Then either tag each event with the distinct + id and session id, or, where the SDK has contexts, open one around the whole + request and identify that context so everything the request reports inherits it. + Where the SDK ships a middleware for this framework, that is what does this — use + it rather than writing your own. Read the docs and the example app. + +## How you know you succeeded + +Identification matches the kind of app you are in. While a user is known, what the +app reports is attributable to them with a stable distinct id — the errors it +reports, not only the events — established at the boundary the SDK gives you rather +than repeated at call sites that already inherit it. Where the app changes who the +user is mid-request, that boundary cannot know it yet, and establishing identity +again there is the point, not a duplicate. Your handoff names the files you +changed, how identity is established in them, and what a later step must do for its +own calls to inherit it — whether that is nothing at all, or tagging each call +itself. If the app has no auth or user concept, say so and stop. + +You do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading. diff --git a/src/programs/posthog-integration/agent/init.md b/src/programs/posthog-integration/agent/init.md new file mode 100644 index 000000000..474081812 --- /dev/null +++ b/src/programs/posthog-integration/agent/init.md @@ -0,0 +1,38 @@ +--- +type: init +flow: integration-v2 +label: Set up PostHog initialization +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-init, posthog-best-practices] +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. + +Try to follow these principles, in order: codebase convention, don't make any +unnecessary changes, keep the change as lean as possible for an easy review, and +follow the shape of the examples provided. How call sites reach the client is +part of the shape: reach it the way the docs and the example project reach it. + +## 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. Your handoff names the +files you changed, how the client is constructed in them, and how a call site +reaches that client — every step after you has to reach the same one the same way. +If the app ships a Content-Security-Policy, the handoff also names exactly +which directives you touched (`script-src`, `connect-src`, `worker-src`) and +which you left alone — the review verifies directive-by-directive, and an +unnamed directive reads as unhandled, not as fine. + +You do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading. diff --git a/src/programs/posthog-integration/agent/install.md b/src/programs/posthog-integration/agent/install.md new file mode 100644 index 000000000..6c6d7274d --- /dev/null +++ b/src/programs/posthog-integration/agent/install.md @@ -0,0 +1,67 @@ +--- +type: install +flow: integration-v2 +label: Add the PostHog SDK to the manifest +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-haiku-4-5 +skills: [integration-v2-install] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Install the PostHog SDK with the project's own package manager, so the real +published version lands in the manifest and the lockfile — not one you made up. +Run the manager's add command with the bare package name and let it resolve the +version: `npm install ` (or the project's pnpm/yarn/bun), `pip install `, +`composer require `, `bundle add `, `gem install `. Never invent a +version — one that was never published fails the whole run with `ETARGET`, and +guessing another just fails again. The only version you write by hand is one copied +from the framework reference example, and only in the environment fallback below. + +Add the client library, plus the server library if the app has server-side code +that sends events. If the SDK is already installed, leave it and say so. + +Some ecosystems pin an explicit version in a manifest no install command resolves +(Swift SPM, Gradle). There, declare the dependency the way the framework reference +shows and take the version from the SDK's latest release — do not invent one. + +Attempt the install once. If it fails on something you own — a wrong package name or +the wrong manager — fix that and run it one more time. + +But if the environment prevents the install through no fault of your change, do NOT +spiral retrying it. These are environment failures, not yours: + +- a peer-dependency conflict the project already had (npm `ERESOLVE`), +- a dependency already in the manifest that itself does not resolve (a bad or yanked + version, a package that no longer exists) — the failure names a package that is not + PostHog, +- the package manager crashing on the project's existing dependency graph or a + corrupt/incompatible lockfile (e.g. npm `Cannot read properties of null`), +- the package manager itself failing to run or provision — corepack unable to + switch to a pinned version (`Failed to switch pnpm to …`, `Cannot find matching + keyid`), a missing manager binary, a toolchain the machine does not have, +- no network or registry reachable. + +The test: if the failure names your PostHog package or your command, it is yours to +fix; if it names another dependency, a version pin the project set, or the toolchain +itself, it is the environment — fall back and move on. + +When you hit one of those, fall back: add the PostHog package to the manifest by hand +at a known-good version, so the dependency is still declared for the later steps and +for the user's own install. Use the framework reference example's spec if it names +one; otherwise use a caret on the SDK's current known-good release — `posthog-js` +`^1.407.2`, `posthog-node` `^5.46.1`, `posthog-php` `^4.12.1`, `posthog` (Python) +`^7.29.0`, `posthog-ruby` `^3.21.0`. The caret keeps it current without pinning a +patch; never invent a version that was never published. Then move on, and say in your +handoff exactly what failed and the command +you ran, so the review step and the report surface it. + +## How you know you succeeded + +The SDK resolved and installed at its real version, or you have reported plainly in +your handoff why the environment stopped it. Your handoff names the manifest and the +package, so later steps import it under the name they will actually get. diff --git a/src/programs/posthog-integration/agent/integrate-posthog.md b/src/programs/posthog-integration/agent/integrate-posthog.md new file mode 100644 index 000000000..712e39238 --- /dev/null +++ b/src/programs/posthog-integration/agent/integrate-posthog.md @@ -0,0 +1,51 @@ +--- +type: integrate-posthog +flow: integration-v2 +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +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` and `error-tracking`, each after `install` and `init` and independent + of one another, so they run in parallel. `error-tracking` makes the errors the app + does not catch reach PostHog, by whatever means the SDK offers for that — it needs + the SDK installed and initialized, not the events. +- `capture`, after `identify` — it decides the events and instruments them, and it + reads how identity is already established before it instruments anything. +- `ai-observability`, after `capture`, parallel to `error-tracking` — it instruments + existing LLM calls, or reports that none apply. Queue it on every default run; the + task checks applicability rather than the planner guessing from a package name. +- `logs`, after `ai-observability` — it configures log capture on supported + platforms, or reports why it does not apply. Queue it even when AIO will be + skipped. These tasks run sequentially because both instrument the same entry + and initialization files, and Logs preserves any tracing provider AIO + configures — an ordering-dependent instruction. (Their dependency installs + are deferred to review, so manifests are not the conflict.) +- `review`, after `install`, `init`, `identify`, `capture`, `error-tracking`, + `ai-observability`, and `logs` — + it installs the dependencies, verifies the project builds/typechecks/lints, and + reviews every change the run made, fixing what fails. There is no separate build + step: verifying and reviewing are one pass over the same changeset. +- `dashboard`, after `capture`, independent of AIO, Logs, and `review` — it builds insights from the + instrumented events, which `capture` has already defined; it needs no code review. +- `report`, after `dashboard` **and** `review` — it writes the setup report last, so + it describes the integration as reviewed rather than as first written. + +If `enqueue_task` does not offer a task's type, this run excludes that product +— skip it rather than retrying, and hang nothing off it. + +## 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/src/programs/posthog-integration/agent/logs.md b/src/programs/posthog-integration/agent/logs.md new file mode 100644 index 000000000..f5750d6cf --- /dev/null +++ b/src/programs/posthog-integration/agent/logs.md @@ -0,0 +1,60 @@ +--- +type: logs +flow: integration-v2 +label: Add log capture +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill] +disallowedTools: [enqueue_task] +dependsOn: [ai-observability] +optional: true +--- + +## Goal + +Configure PostHog log capture for this project's supported runtimes. Load the +`logs` skill menu, install its matching skill, and read the platform reference +before editing. Inspect the runtime and existing logging setup; use the +documented OTLP exporter or SDK-native log capture for that platform. If the +app already uses OpenTelemetry, write against its existing OTel version and +name exporter packages compatible with it — never require, or write code that +assumes, an upgrade of a package this integration did not introduce. A +browser-only app must not receive a server exporter. If no documented setup +applies, complete as `not needed` and explain why. + +Export only log lines this integration adds. Create a dedicated logger for the +PostHog exporter and emit a few purpose-written lines at meaningful points the +app already passes through. Even where the platform reference attaches the +exporter to the application's root or existing loggers, do not — the app's +existing logs are data this run has no mandate to export, and every line that +leaves must be visible in this run's diff. Reuse any existing PostHog log +export and leave all existing handlers and outputs intact; if capture is +already configured, report that without adding a second exporter. Preserve any +tracing provider AIO configured in the previous step. Do not export secrets, +request bodies, or user data, and do not scatter logs through unrelated code. + +This is the instrumentation part of a larger integration. Instead of the +standalone skill's install command, name the required packages in your handoff +and leave installation, manifest and lockfile updates, builds, and lint to +review. Do not edit dependency manifests or write a version number you +invented — the package manager resolves real versions when review installs by +name. Use `detect_package_manager` if needed. Use +the wizard's supplied project and region and the env names from upstream +handoffs; inspect and write environment files only through `check_env_keys` +and `set_env_values`. Do not request credentials or select a different project. +If required configuration is unavailable, report what is missing instead of +guessing a region. + +## How you know you succeeded + +Log capture is configured once for the supported runtime, was already present, +or has a clear skip reason. The handoff names the changed files, packages, env +variable names (never values), and any dependency work for review. Give a +specific code path the user can trigger to produce one of the added log lines +and find it in PostHog Logs; delivery remains unverified until observed. Note +for the report that only lines added by this run are exported, and the one-line +change that routes the app's existing loggers into the same exporter if the +user wants more. Do not publish a separate report. diff --git a/src/programs/posthog-integration/agent/report.md b/src/programs/posthog-integration/agent/report.md new file mode 100644 index 000000000..25bda1924 --- /dev/null +++ b/src/programs/posthog-integration/agent/report.md @@ -0,0 +1,53 @@ +--- +type: report +flow: integration-v2 +label: Write the setup report +sink: true +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-report, integration-v2-notebook, integration-v2-mcp] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [enqueue_task] +dependsOn: [dashboard, review] +--- + +## Goal + +Compose the setup report summarizing what this integration did, drawing only on +what the run itself recorded: the queue log and event plan in +`.posthog-wizard-cache/` (`queue.json` and `.posthog-events.json`), and the +handoff each step left behind. Then publish it with a single `publish_handoff` +call. The same call mirrors the markdown into a shareable PostHog notebook. The notebook skill says what the result +means and covers older wizards. Do not write a report file. The handoff call +and the notebook are how the report reaches the user. + +Separate what the run verified from what it did not. A passing build proves the +code compiles, not that events flow — never write that an event was captured +unless the run observed it arrive. Where a step failed or was skipped, say so +plainly instead of rounding it up to success. Anything only the user can confirm +belongs in a checklist to work through before merging, each item naming the file +and line to look at. + +Where a handoff carries a report section, that step already wrote the part of +the report it owns. Include it as its own section instead of rewriting or +summarizing it, and do not restate its contents elsewhere. + +Where a handoff reports something a step could not resolve — attribution it could not +establish, a question the step before it left open — that is not a caveat to bury in +prose. Raise it as its own issue to follow up, saying what is unresolved and what it +costs if left alone. A `DISTINCT_ID` placeholder left at a call site means no stable id +was available: name every file and line carrying one, so the user knows what to replace +before those events mean anything. + +## How you know you succeeded + +One `publish_handoff` call went through with the full report: what was +installed and initialized, the events captured, whether identify was wired or +skipped, error tracking added, AI Observability and Logs configured, skipped, or +failed with reasons, the dashboard link, any build conflict in full, +and the next steps for the user. Every claim in it traces to a handoff, and +what the run could not confirm reads as unconfirmed. The notebook mirror is +best effort: when it was created, the user has its link, and when it wasn't, +your final message says why, and when an update failed, that the notebook holds the earlier version. A missing notebook never fails this task. diff --git a/src/programs/posthog-integration/agent/review.md b/src/programs/posthog-integration/agent/review.md new file mode 100644 index 000000000..b36ec7c19 --- /dev/null +++ b/src/programs/posthog-integration/agent/review.md @@ -0,0 +1,104 @@ +--- +type: review +flow: integration-v2 +label: Verify and review the integration +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [posthog-best-practices, integration-v2-build] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [install, init, identify, error-tracking, capture, ai-observability, logs] +--- + +## Goal + +You are the last hands on the code. First make the integration run, then review it, +then leave it building to the best of your ability. Do both in one pass, verifying and reviewing are the same +reading of the same changeset. + +**Install and verify.** The earlier steps edited code but did not install — +install now, then verify the project builds, typechecks, and lints, whichever +of those it defines. Install what the manifest declares plus every package the +upstream handoffs name, adding the named ones by bare name with the project's +package manager so it resolves real versions. The app's existing dependencies +are not yours to touch: never upgrade, downgrade, or re-add a package this +integration did not introduce. If an instrumentation package's peer range +conflicts with a dependency the app already has, install an older +instrumentation version that fits the app's existing versions — find it with +the package manager, stepping down major versions (`pkg@^7`, then `@^6`, +…) for a few attempts rather than looking for documentation of one. If none +fits, revert that piece of instrumentation, leave the app's dependency alone, +and record the incompatibility so the report can say what was skipped and why. An optional upstream task +(AI Observability, Logs) may have failed and left partial edits no handoff +describes — install and build either way, and let the build surface stray +imports; fix or revert them like any other defect. Do not run the test suite; the +runtime does not allow it, and a green build is the bar. A bad version only surfaces +here: if the install fails because a declared version does not exist (npm `ETARGET`, +"no matching version", a yanked release), do not guess pins — reinstall the +package by bare name and let the package manager pick, matching the framework +reference example's spec only if that also fails. If the failure is the environment, not the +integration's own change (a pre-existing broken dependency, a package manager that +cannot provision), note it and move on; do not spiral. + +**Review, as a strict reviewer who did none of the writing.** The changeset is +already handed to you: `read_handoffs` returns every upstream task's handoff, and +each one names the files it touched and what it changed in them. That union of +touched files IS the changeset. Read each changeset file once and judge it — after +your own edit you already know its contents, so do not re-read it. Read beyond the +changeset only where a dimension sends you: one neighboring example per pattern you +are checking, and the callers of code that changed. Nothing else — not the SDK's +installed internals, not other tasks' instructions, not a diff of the whole tree; +the handoffs are the signal. +The handoffs also carry what no file shows — env values written through +`set_env_values`, manifest edits. Judge each change against these dimensions, in +order: + +1. Minimal — every line earns its place. Delete helpers nothing calls, defensive + wrapping around code that cannot fail, config for features that were not set up, + and comments that narrate what a line does or that it was added. +2. Unrelated — the run must not have changed behavior outside the integration. + Revert reformatting, renames, or edits in code the integration never needed. +3. Codebase patterns — for each kind of edit (client construction, how call sites + reach it, config and env access, imports, naming, comment density), find one + neighboring example in this project and compare. If a change invented a seam the + project does not have — a registry, locator, wrapper, or app the codebase never + uses — replace it with the closest pattern the project actually uses. +4. Example shapes — where the framework reference example shows a shape for init or + instrumentation, the code should be recognizably that shape adapted to this + codebase. Codebase idiom beats the example when they conflict; the PostHog + correctness rules you were given beat both. + +You never change what is captured — event names, properties, and where events fire +are a contract the dashboard and report are built on. Refactor how, and kill bad +changes; never alter the what. + +Events reaching PostHog in production is the point of this code, so a capture wired +to a place it will never fire, an uninitialized SDK, or a call the runtime silently +drops is squarely yours to catch and fix — it is a code defect. What you cannot do is +confirm delivery: you cannot exercise the app, and you never treat "I could not +verify events arrive" as a finding. + +Stay inside verify-and-review. These are not your job: + +- The report or the dashboard — other tasks own those; do not write or judge them. +- Product or analytics design — which events are worth tracking, identity strategy, + framework-version tradeoffs. The changeset is the decision; review whether the code + implements it correctly, not whether it was the right decision. +- Anything the integration did not touch. Do not widen scope to pre-existing issues. + +If you notice something real but out of scope, leave it — a one-line mention in your +handoff, no more. Only flag what you can pin to a specific line and dimension — no +taste-based rewrites, and never expand the integration. For each fix, ask what would +behave differently if it were wrong, and check by reading the callers. After any +change, re-run the build or lint and confirm it still passes. + +## How you know you succeeded + +The project installed and builds, typechecks, and lints as well as it did before the +run; you can name each review dimension and say it passes; and your fixes are in the +files. If the build or lint fails only on pre-existing errors you did not introduce, +that still counts — put a one-line summary in the `conflict` field and the detail in +what you did. Reserve a failed status for when your own changes break the build. Put +the number of fixes and the dimension each addressed in your handoff. diff --git a/src/programs/posthog-integration/agent/warehouse.md b/src/programs/posthog-integration/agent/warehouse.md new file mode 100644 index 000000000..9079ae5a4 --- /dev/null +++ b/src/programs/posthog-integration/agent/warehouse.md @@ -0,0 +1,41 @@ +--- +type: warehouse +flow: integration-v2 +label: Connect your data sources +runnerSeeded: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-warehouse, integration-v2-mcp] +allowedTools: [Read, Glob, Grep, wizard_ask, posthog_exec] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [] +--- + +## Goal + +Connect the data sources listed in your task input to PostHog's data warehouse, +so the project's own data can be queried next to the events this run +instruments. The wizard found them in the project; your job is to turn each one +into a real connection, or into a link the user can finish in the browser. + +You are the only step that stops to ask the user for anything, so make the ask +count: send the user to that source's secure connect page in one `wizard_ask` +call, tagged with that source's kind as its `subject`. The user enters the +credentials on that page, so no secret passes through you. A decline answers +that one source. Move on to the next source rather than re-asking or ending the +round. + +Touch no project code. This step connects data; it does not edit the app. + +## How you know you succeeded + +Every source in your input reached a decided end: connected in PostHog, handed +back as a link to finish, or skipped with a reason. Your handoff carries a +finished report section naming each source and which of those three it was, so +the reporting step can include it as it stands. + +A link is a handoff, not a connection. Your report section opens with the count +you connected, and your `complete_task` status matches it: `done` only when you +connected at least one source. diff --git a/src/programs/posthog-integration/flow.ts b/src/programs/posthog-integration/flow.ts new file mode 100644 index 000000000..e2a64b96e --- /dev/null +++ b/src/programs/posthog-integration/flow.ts @@ -0,0 +1,53 @@ +import { WIZARD_DEFAULT_AIO_LOGS_FLAG_KEY } from '@shared/constants'; +import { defineFlow, type FlowContext } from '@programs/program-contract'; + +/** Kill switch over the shipped default: only an explicit 'false' excludes AI Observability and Logs. */ +const aioLogsEnabled = ({ flags }: FlowContext) => + flags[WIZARD_DEFAULT_AIO_LOGS_FLAG_KEY] !== 'false'; + +/** The default integration: what the `integrate-posthog` planner prompt describes, as a graph. */ +export const flow = defineFlow({ + sink: 'report', + nodes: { + install: {}, + init: {}, + identify: { dependsOn: ['install', 'init'] }, + 'error-tracking': { dependsOn: ['install', 'init'] }, + capture: { dependsOn: ['identify'] }, + 'ai-observability': { + dependsOn: ['capture'], + when: (ctx) => + aioLogsEnabled(ctx) && ctx.traits.callsLlmApi?.value !== 'no', + }, + // After AI Observability: both edit the same entry and init files. + logs: { dependsOn: ['ai-observability'], when: aioLogsEnabled }, + review: { + dependsOn: [ + 'install', + 'init', + 'identify', + 'capture', + 'error-tracking', + 'ai-observability', + 'logs', + ], + }, + dashboard: { dependsOn: ['capture'] }, + // Queued by the wizard from detected sources; runs after everything but the report. + warehouse: { + picked: true, + dependsOn: [ + 'install', + 'init', + 'identify', + 'error-tracking', + 'capture', + 'ai-observability', + 'logs', + 'review', + 'dashboard', + ], + }, + report: { dependsOn: ['dashboard', 'review', 'warehouse'] }, + }, +}); diff --git a/src/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts index 53bc75a88..cf546f069 100644 --- a/src/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -36,6 +36,7 @@ import { getDetectedWarehouseSources } from '@services/warehouse-sources'; import { buildCodingAgentPrompt } from './handoff.js'; import { EVENT_PLAN_FILE } from './constants.js'; import { WAREHOUSE_SOURCE_SCOPE_ADDITIONS } from '../shared/program-scopes'; +import { flow } from './flow'; const DASHBOARD_DEEP_LINK_KEY = 'dashboardDeepLink'; @@ -245,6 +246,8 @@ export const config: ProgramConfig = { description: 'Set up PostHog SDK integration', id: 'posthog-integration', agentFlow: 'integration-v2', + agentPrompts: () => import('./agent-prompts').then((m) => m.AGENT_PROMPTS), + flow, handoffNotebook: true, eventPlanFile: EVENT_PLAN_FILE, scansCodebase: true, diff --git a/src/programs/program-contract/flow.ts b/src/programs/program-contract/flow.ts new file mode 100644 index 000000000..185b903fc --- /dev/null +++ b/src/programs/program-contract/flow.ts @@ -0,0 +1,73 @@ +/** + * A program's orchestrator flow as code: one node per task type, keyed by the + * type, so a misspelled dependency fails the build. `resolveFlow` applies the + * run's project traits and flags and hands the orchestrator a `TaskGraph`. + */ + +import type { TaskGraph } from '@agent/types'; +import type { SkillCriteria } from '@shared/skill-resolution'; +import type { ProjectTraits } from './project-traits'; + +export type FlowContext = { + traits: ProjectTraits; + flags: Readonly>; +}; + +export type FlowNode = { + /** The types this one runs after. A dropped or unqueued dependency stands in for its own. */ + dependsOn?: readonly K[]; + /** Match each dependency to the tasks of its type that share these input values. */ + matchInputs?: readonly string[]; + /** The planner or the caller decides whether it is queued, not the graph. */ + picked?: true; + /** Runs only when this holds. A missing trait must read as "run the node". */ + when?: (ctx: FlowContext) => boolean; + /** The node's skills by criteria; until set, its prompt's frontmatter names them. */ + skills?: readonly SkillCriteria[]; +}; + +export type Flow = { + /** The task that runs last and reports on the run. */ + sink: K; + nodes: { readonly [P in K]: FlowNode }; +}; + +/** Declare a flow; keys are task types, and every `dependsOn` and `sink` must name one. */ +export function defineFlow(flow: Flow): Flow { + return flow; +} + +/** The flow for this run: nodes whose `when` fails are excluded, and their dependents inherit their edges. */ +export function resolveFlow(flow: Flow, ctx: FlowContext): TaskGraph { + const nodes = flow.nodes as Readonly>>; + const excluded = Object.keys(nodes).filter( + (type) => nodes[type].when?.(ctx) === false, + ); + const dropped = new Set(excluded); + const spliced = (type: string, seen = new Set()): string[] => + (nodes[type]?.dependsOn ?? []).flatMap((dep) => { + if (seen.has(dep)) return []; + seen.add(dep); + return dropped.has(dep) ? spliced(dep, seen) : [dep]; + }); + + return { + sink: flow.sink, + excluded, + nodes: Object.fromEntries( + Object.keys(nodes) + .filter((type) => !dropped.has(type)) + .map((type) => { + const node = nodes[type]; + return [ + type, + { + dependsOn: [...new Set(spliced(type))], + ...(node.matchInputs ? { matchInputs: node.matchInputs } : {}), + ...(node.picked ? { picked: true } : {}), + }, + ]; + }), + ), + }; +} diff --git a/src/programs/program-contract/index.ts b/src/programs/program-contract/index.ts index b086ccd51..d614ca3e9 100644 --- a/src/programs/program-contract/index.ts +++ b/src/programs/program-contract/index.ts @@ -15,6 +15,13 @@ export type { TraitName, TraitSource, } from './project-traits'; +export { + defineFlow, + resolveFlow, + type Flow, + type FlowContext, + type FlowNode, +} from './flow'; export type { DecisionQuestionKey, ProgramConfig, diff --git a/src/programs/program-contract/program-step.ts b/src/programs/program-contract/program-step.ts index 486167826..360ab314c 100644 --- a/src/programs/program-contract/program-step.ts +++ b/src/programs/program-contract/program-step.ts @@ -6,6 +6,7 @@ import type { Integration } from '@shared/constants'; import type { FrameworkConfig } from './framework-config'; import type { CiRunnerContext, RunnerContext } from './runner-context'; import type { TraitSource } from './project-traits'; +import type { Flow } from './flow'; /** A question a program may ask the Decisions service; the catalog in `@services/detection/decisions` has exactly these keys. */ export type DecisionQuestionKey = @@ -237,6 +238,10 @@ export interface ProgramConfig { * runner only applies it. */ excludedTaskTypes?: (flags: Record) => readonly string[]; + /** This program's orchestrator task graph; see `./flow`. */ + flow?: Flow; + /** This program's agent prompts by task type, from its `agent/` folder; loaded on first use. */ + agentPrompts?: () => Promise>>; /** Prerequisites: other program ids that must have run first */ requires?: string[]; /** diff --git a/src/programs/program-runner/run-program.ts b/src/programs/program-runner/run-program.ts index 619753883..7a38f3ab9 100644 --- a/src/programs/program-runner/run-program.ts +++ b/src/programs/program-runner/run-program.ts @@ -37,7 +37,7 @@ import { logToFile } from '@utils/debug'; import { removeAuditLedger, startAuditLedgerWatcher } from '@programs/audit'; import { FRAMEWORK_REGISTRY } from '@services/frameworks'; import { findProgramConfig, getProgramConfig } from './program-registry'; -import { ProgramAbort } from '@programs/program-contract'; +import { ProgramAbort, resolveFlow } from '@programs/program-contract'; import { detectionFailure, detectProgram } from './detect-program'; import { rotateCredentials, @@ -415,6 +415,7 @@ async function invokeProgram( program_id: planned.config.id, }); } + const programFlow = planned.config.flow; const result = await runAgent( { programId: planned.config.id, @@ -437,6 +438,14 @@ async function invokeProgram( handoffNotebook: agentRun.handoffNotebook ?? planned.config.handoffNotebook, agentFlow: planned.config.agentFlow, + agentPrompts: planned.config.agentPrompts, + taskGraph: programFlow + ? () => + resolveFlow(programFlow, { + traits: getProjectTraits(session()), + flags: flags.flags, + }) + : undefined, excludedTaskTypes: planned.config.excludedTaskTypes, seedTasks: planned.config.seedTasks ? () => planned.config.seedTasks!(session()) diff --git a/src/programs/replay-vision/agent-prompts.ts b/src/programs/replay-vision/agent-prompts.ts new file mode 100644 index 000000000..72cd3bb87 --- /dev/null +++ b/src/programs/replay-vision/agent-prompts.ts @@ -0,0 +1,20 @@ +// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. + +/** This program's agent prompts by task type, verbatim. */ +export const AGENT_PROMPTS: Readonly> = { + 'enable-replay': + '---\ntype: enable-replay\nflow: replay-vision\nlabel: Make session replay record\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-enable-replay]\nallowedTools: [Read, Edit, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Bash, enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nMake session replay actually record. Recording has a server half and a client\nhalf, and either one can silently cancel the other.\n\nServer half: call `products-enable` (via `exec`: `info products-enable`, then\n`call products-enable {"products": ["session_replay"]}`). It is idempotent and\nserver-owned; `"enabled"` and `"already_enabled"` are both success.\n\n- **Tool missing on this deploy**: don\'t fail the task — record a follow-up in\n your handoff telling the user to turn on "Record user sessions" under\n Settings → Session replay, and finish.\n- **Permission rejection**: record a follow-up to enable it from a\n project-admin account, and finish — scanners created later sit idle until\n recording is on, then start working with no re-setup.\n\nClient half, web apps only: check the `posthog.init(...)` options.\n`disable_session_recording: true` cancels the server flip — remove it (or set\nit `false`). If nothing overrides recording, leave the init alone. This is the\nonly code edit this task may make: never restructure the init, never add new\ninstrumentation. If you can\'t confidently locate or edit the init, record a\nfollow-up instead of guessing.\n\nPure backend or mobile app with no web surface: nothing records browser\nsessions here. Say so in your handoff and finish — scanner tasks read your\nhandoff and skip what doesn\'t apply.\n\n## How you know you succeeded\n\nRecording is on server-side and nothing in the client init cancels it — or\nyour handoff names exactly which half needs the user and why. Later tasks\ntrust your handoff, so state plainly whether recordings will flow.\n', + init: "---\ntype: init\nflow: replay-vision\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure PostHog is initialized. If the project already has a working\n`posthog.init(...)` (or the framework's equivalent) with its env keys wired,\nleave it alone and say so in your handoff. If it doesn't, create it following\nyour skill — it owns the how: the framework's init point, the env-var wiring\nthrough the wizard tools, and `.env.example`.\n\nYou only exist in this flow because the user asked for Replay vision on a\nrepo without PostHog. Initialize the SDK so recordings can flow and stop —\nno instrumentation, no extras. Don't touch session-recording options either\nway; the enable-replay task after you owns those.\n\n## How you know you succeeded\n\nAn init point exists with the PostHog env keys present — whether it already\ndid or you just created it — keys in the env file, never hardcoded. Your\nhandoff names the files involved and how the client is constructed, so the\nenable-replay task can find the init options without re-discovering them.\n", + install: + "---\ntype: install\nflow: replay-vision\nlabel: Add the PostHog SDK to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-haiku-4-5\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure the PostHog SDK is in the manifest. If it's already installed,\nleave it alone and say so in your handoff. If it isn't, install it following\nyour skill — it owns the how: the package manager rules, the version rules,\nwhat counts as an environment failure, and the fallback.\n\nYou only exist in this flow because the user asked for Replay vision on a\nrepo without PostHog. Install the client library (plus the server library if\nthe app sends events server-side) and stop — no instrumentation, no extras.\n\n## How you know you succeeded\n\nThe SDK is declared in the manifest at a real version — whether it already\nwas or you just installed it — or your handoff plainly says why the\nenvironment stopped you. Your handoff names the manifest and the package, so\nlater steps import it under the name they will actually get.\n", + report: + '---\ntype: report\nflow: replay-vision\nlabel: Report and hand off\nsink: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Glob, Grep, Write, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: [scanner-broken-experiences, scanner-user-frustration, scanner-session-summaries]\n---\n\n## Goal\n\nTell the user what Replay vision is now doing for them, from the handoffs of\nevery task in the run. Briefly and concretely:\n\n- What is now recording (or the follow-up needed to make it record).\n- Each scanner created or updated: its name, what it watches, its query\n scope, and its estimated monthly credit spend.\n- Anything skipped or deferred, with the reason — a skipped scanner on a\n backend-only project is an outcome, not an apology.\n- Where results appear: the Replay vision page in PostHog, with the first\n observations arriving as new recordings complete.\n\nIf the run also installed and initialized the SDK, say so — the user started\nthis command without PostHog and now has it.\n\nWrite the report to `./posthog-replay-vision-report.md` (the wizard shows this\nfile at the end of the run), then publish the same content as your handoff.\n\n## How you know you succeeded\n\nA user who reads only your report knows what watches their product, what it\ncosts, what still needs them, and where to look first.\n', + 'scanner-broken-experiences': + "---\ntype: scanner-broken-experiences\nflow: replay-vision\nlabel: Breakage scanner\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-scanners-core, replay-vision-scanner-broken-experiences]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [enable-replay]\n---\n\n## Goal\n\nCreate the breakage monitor: the product visibly breaking, on the flow where\nbreaking costs the most — named and written for this product. The brief, the\nlocked scaffold, and the create/re-run mechanics are in your skills — you\nfill the blanks from the repo:\n\n- **`name`** — what it watches, in this product's own words.\n- **`query`** — this product's key completion flow plus its immediate\n predecessors. Checkout, signup, booking, publish — whatever this product's\n \"done\" is. Read router files and page/route directories to find the real\n paths; never guess at `/checkout` if this app calls it `/booking`.\n- **`{{WATCH_FOR}}`** — the concrete failure modes of this flow, on screen,\n in the product's vocabulary.\n- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence.\n\nIf there is no identifiable completion flow, don't invent one — fall back to\nthe handful of highest-traffic paths and record in your handoff that you\ncouldn't identify a completion flow. If the enable-replay handoff says this\nproduct has no web surface, skip creation entirely and say why — that is a\ncorrect outcome.\n\n## How you know you succeeded\n\nThe scanner exists in PostHog scoped to this product's real completion flow,\nor your handoff says exactly why it was skipped or deferred.\n", + 'scanner-session-summaries': + "---\ntype: scanner-session-summaries\nflow: replay-vision\nlabel: Session summaries scanner\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-scanners-core, replay-vision-scanner-session-summaries]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [enable-replay]\n---\n\n## Goal\n\nCreate the summary scanner: a rolling sample of plain-language session\nsummaries in this product's own vocabulary, so the user sees the breadth of\nwhat scanners produce. The brief, the locked scaffold, and the create/re-run\nmechanics are in your skills — you fill the blanks from the repo:\n\n- **`name`** — in this product's own words.\n- **`{{VOCABULARY}}`** — the nouns and verbs this product's UI uses for its\n key flows, so summaries read like the product.\n- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence.\n\nIt samples every recording with no query scope, kept cheap by a low sampling\nrate — **never raise the sampling rate during setup**.\n\nIf the enable-replay handoff says this product has no web surface and no\nrecorded web sessions at all, skip creation and say why — that is a correct\noutcome.\n\n## How you know you succeeded\n\nThe scanner exists in PostHog at the brief's sampling rate, or your\nhandoff says exactly why it was skipped or deferred.\n", + 'scanner-user-frustration': + "---\ntype: scanner-user-frustration\nflow: replay-vision\nlabel: User frustration scanner\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-scanners-core, replay-vision-scanner-user-frustration]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [enable-replay]\n---\n\n## Goal\n\nCreate the frustration monitor: the user getting stuck, named and written\nfor this product. It is gated on `$rageclick` — cheap and high-precision,\nbecause here the gating event *is* the friction. The brief, the locked\nscaffold, and the create/re-run mechanics are in your skills — you fill the\nblanks from the repo:\n\n- **`name`** — what it watches, in this product's own words.\n- **`{{STUCK_MOMENTS}}`** — where a user of this product realistically gets\n stuck, on screen, in the product's vocabulary.\n- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence.\n\nIf the enable-replay handoff says this product has no web surface and no\nrecorded web sessions at all, skip creation and say why — that is a correct\noutcome.\n\n## How you know you succeeded\n\nThe scanner exists in PostHog gated on `$rageclick` alone, or your handoff\nsays exactly why it was skipped or deferred.\n", + 'setup-replay-vision': + '---\ntype: setup-replay-vision\nflow: replay-vision\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan a Replay vision setup and seed the task queue. A scanner is an LLM that\nwatches one session recording at a time and writes an observation — a score, a\ntag, or a summary — and every observation lands as a queryable PostHog event.\nScanners only see what session replay records, so the plan has two halves:\nmake sure recordings flow, then create scanners tailored to this product.\n\nFirst, establish one fact from the repo: **is PostHog already integrated?**\nLook for `posthog-js` or a server SDK in the dependency manifests, or a\n`posthog.init(...)` / snippet in the source. Check the project state for\nexisting events if the repo is ambiguous.\n\nThen seed one of two graphs:\n\n**PostHog is already integrated** — the common case:\n\n- `enable-replay`, with no dependencies.\n- The three scanner tasks — `scanner-broken-experiences`,\n `scanner-user-frustration`, `scanner-session-summaries` — each after\n `enable-replay`, independent of one another, so they run in parallel.\n- `report`, after all three scanner tasks. It writes the handoff last, so it\n describes what actually shipped.\n\n**PostHog is not integrated** — do not stop, integrate:\n\n- `install` and `init`, independent of each other.\n- `enable-replay`, after `install` and `init`.\n- The three scanner tasks and `report`, wired exactly as above.\n\nNever plan an identify, capture, or dashboard task — this run sets up Replay\nvision, not the full integration. The minimal SDK footprint that `install` and\n`init` leave behind is enough for recordings to flow.\n\n## How you know you succeeded\n\nEvery task in the chosen graph is queued with that dependency shape, the\nreport last, and the first task runnable. Keep labels short — the action in a\nfew words.\n', +}; diff --git a/src/programs/replay-vision/agent/enable-replay.md b/src/programs/replay-vision/agent/enable-replay.md new file mode 100644 index 000000000..666c64e67 --- /dev/null +++ b/src/programs/replay-vision/agent/enable-replay.md @@ -0,0 +1,46 @@ +--- +type: enable-replay +flow: replay-vision +label: Make session replay record +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [replay-vision-enable-replay] +allowedTools: [Read, Edit, Glob, Grep, posthog_exec] +disallowedTools: [Write, Bash, enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Make session replay actually record. Recording has a server half and a client +half, and either one can silently cancel the other. + +Server half: call `products-enable` (via `exec`: `info products-enable`, then +`call products-enable {"products": ["session_replay"]}`). It is idempotent and +server-owned; `"enabled"` and `"already_enabled"` are both success. + +- **Tool missing on this deploy**: don't fail the task — record a follow-up in + your handoff telling the user to turn on "Record user sessions" under + Settings → Session replay, and finish. +- **Permission rejection**: record a follow-up to enable it from a + project-admin account, and finish — scanners created later sit idle until + recording is on, then start working with no re-setup. + +Client half, web apps only: check the `posthog.init(...)` options. +`disable_session_recording: true` cancels the server flip — remove it (or set +it `false`). If nothing overrides recording, leave the init alone. This is the +only code edit this task may make: never restructure the init, never add new +instrumentation. If you can't confidently locate or edit the init, record a +follow-up instead of guessing. + +Pure backend or mobile app with no web surface: nothing records browser +sessions here. Say so in your handoff and finish — scanner tasks read your +handoff and skip what doesn't apply. + +## How you know you succeeded + +Recording is on server-side and nothing in the client init cancels it — or +your handoff names exactly which half needs the user and why. Later tasks +trust your handoff, so state plainly whether recordings will flow. diff --git a/src/programs/replay-vision/agent/init.md b/src/programs/replay-vision/agent/init.md new file mode 100644 index 000000000..b4c614142 --- /dev/null +++ b/src/programs/replay-vision/agent/init.md @@ -0,0 +1,33 @@ +--- +type: init +flow: replay-vision +label: Set up PostHog initialization +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-init, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure PostHog is initialized. If the project already has a working +`posthog.init(...)` (or the framework's equivalent) with its env keys wired, +leave it alone and say so in your handoff. If it doesn't, create it following +your skill — it owns the how: the framework's init point, the env-var wiring +through the wizard tools, and `.env.example`. + +You only exist in this flow because the user asked for Replay vision on a +repo without PostHog. Initialize the SDK so recordings can flow and stop — +no instrumentation, no extras. Don't touch session-recording options either +way; the enable-replay task after you owns those. + +## How you know you succeeded + +An init point exists with the PostHog env keys present — whether it already +did or you just created it — keys in the env file, never hardcoded. Your +handoff names the files involved and how the client is constructed, so the +enable-replay task can find the init options without re-discovering them. diff --git a/src/programs/replay-vision/agent/install.md b/src/programs/replay-vision/agent/install.md new file mode 100644 index 000000000..7cbd0230e --- /dev/null +++ b/src/programs/replay-vision/agent/install.md @@ -0,0 +1,30 @@ +--- +type: install +flow: replay-vision +label: Add the PostHog SDK to the manifest +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-haiku-4-5 +skills: [integration-v2-install] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure the PostHog SDK is in the manifest. If it's already installed, +leave it alone and say so in your handoff. If it isn't, install it following +your skill — it owns the how: the package manager rules, the version rules, +what counts as an environment failure, and the fallback. + +You only exist in this flow because the user asked for Replay vision on a +repo without PostHog. Install the client library (plus the server library if +the app sends events server-side) and stop — no instrumentation, no extras. + +## How you know you succeeded + +The SDK is declared in the manifest at a real version — whether it already +was or you just installed it — or your handoff plainly says why the +environment stopped you. Your handoff names the manifest and the package, so +later steps import it under the name they will actually get. diff --git a/src/programs/replay-vision/agent/report.md b/src/programs/replay-vision/agent/report.md new file mode 100644 index 000000000..010a9a2ab --- /dev/null +++ b/src/programs/replay-vision/agent/report.md @@ -0,0 +1,38 @@ +--- +type: report +flow: replay-vision +label: Report and hand off +sink: true +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Glob, Grep, Write, posthog_exec] +disallowedTools: [enqueue_task] +dependsOn: [scanner-broken-experiences, scanner-user-frustration, scanner-session-summaries] +--- + +## Goal + +Tell the user what Replay vision is now doing for them, from the handoffs of +every task in the run. Briefly and concretely: + +- What is now recording (or the follow-up needed to make it record). +- Each scanner created or updated: its name, what it watches, its query + scope, and its estimated monthly credit spend. +- Anything skipped or deferred, with the reason — a skipped scanner on a + backend-only project is an outcome, not an apology. +- Where results appear: the Replay vision page in PostHog, with the first + observations arriving as new recordings complete. + +If the run also installed and initialized the SDK, say so — the user started +this command without PostHog and now has it. + +Write the report to `./posthog-replay-vision-report.md` (the wizard shows this +file at the end of the run), then publish the same content as your handoff. + +## How you know you succeeded + +A user who reads only your report knows what watches their product, what it +costs, what still needs them, and where to look first. diff --git a/src/programs/replay-vision/agent/scanner-broken-experiences.md b/src/programs/replay-vision/agent/scanner-broken-experiences.md new file mode 100644 index 000000000..da3dbb7ff --- /dev/null +++ b/src/programs/replay-vision/agent/scanner-broken-experiences.md @@ -0,0 +1,40 @@ +--- +type: scanner-broken-experiences +flow: replay-vision +label: Breakage scanner +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [replay-vision-scanners-core, replay-vision-scanner-broken-experiences] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [enable-replay] +--- + +## Goal + +Create the breakage monitor: the product visibly breaking, on the flow where +breaking costs the most — named and written for this product. The brief, the +locked scaffold, and the create/re-run mechanics are in your skills — you +fill the blanks from the repo: + +- **`name`** — what it watches, in this product's own words. +- **`query`** — this product's key completion flow plus its immediate + predecessors. Checkout, signup, booking, publish — whatever this product's + "done" is. Read router files and page/route directories to find the real + paths; never guess at `/checkout` if this app calls it `/booking`. +- **`{{WATCH_FOR}}`** — the concrete failure modes of this flow, on screen, + in the product's vocabulary. +- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence. + +If there is no identifiable completion flow, don't invent one — fall back to +the handful of highest-traffic paths and record in your handoff that you +couldn't identify a completion flow. If the enable-replay handoff says this +product has no web surface, skip creation entirely and say why — that is a +correct outcome. + +## How you know you succeeded + +The scanner exists in PostHog scoped to this product's real completion flow, +or your handoff says exactly why it was skipped or deferred. diff --git a/src/programs/replay-vision/agent/scanner-session-summaries.md b/src/programs/replay-vision/agent/scanner-session-summaries.md new file mode 100644 index 000000000..529c81738 --- /dev/null +++ b/src/programs/replay-vision/agent/scanner-session-summaries.md @@ -0,0 +1,37 @@ +--- +type: scanner-session-summaries +flow: replay-vision +label: Session summaries scanner +model_pi: openai/gpt-5.6-terra +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [replay-vision-scanners-core, replay-vision-scanner-session-summaries] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [enable-replay] +--- + +## Goal + +Create the summary scanner: a rolling sample of plain-language session +summaries in this product's own vocabulary, so the user sees the breadth of +what scanners produce. The brief, the locked scaffold, and the create/re-run +mechanics are in your skills — you fill the blanks from the repo: + +- **`name`** — in this product's own words. +- **`{{VOCABULARY}}`** — the nouns and verbs this product's UI uses for its + key flows, so summaries read like the product. +- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence. + +It samples every recording with no query scope, kept cheap by a low sampling +rate — **never raise the sampling rate during setup**. + +If the enable-replay handoff says this product has no web surface and no +recorded web sessions at all, skip creation and say why — that is a correct +outcome. + +## How you know you succeeded + +The scanner exists in PostHog at the brief's sampling rate, or your +handoff says exactly why it was skipped or deferred. diff --git a/src/programs/replay-vision/agent/scanner-user-frustration.md b/src/programs/replay-vision/agent/scanner-user-frustration.md new file mode 100644 index 000000000..99cc739ee --- /dev/null +++ b/src/programs/replay-vision/agent/scanner-user-frustration.md @@ -0,0 +1,35 @@ +--- +type: scanner-user-frustration +flow: replay-vision +label: User frustration scanner +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [replay-vision-scanners-core, replay-vision-scanner-user-frustration] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [enable-replay] +--- + +## Goal + +Create the frustration monitor: the user getting stuck, named and written +for this product. It is gated on `$rageclick` — cheap and high-precision, +because here the gating event *is* the friction. The brief, the locked +scaffold, and the create/re-run mechanics are in your skills — you fill the +blanks from the repo: + +- **`name`** — what it watches, in this product's own words. +- **`{{STUCK_MOMENTS}}`** — where a user of this product realistically gets + stuck, on screen, in the product's vocabulary. +- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence. + +If the enable-replay handoff says this product has no web surface and no +recorded web sessions at all, skip creation and say why — that is a correct +outcome. + +## How you know you succeeded + +The scanner exists in PostHog gated on `$rageclick` alone, or your handoff +says exactly why it was skipped or deferred. diff --git a/src/programs/replay-vision/agent/setup-replay-vision.md b/src/programs/replay-vision/agent/setup-replay-vision.md new file mode 100644 index 000000000..2719c1d9d --- /dev/null +++ b/src/programs/replay-vision/agent/setup-replay-vision.md @@ -0,0 +1,53 @@ +--- +type: setup-replay-vision +flow: replay-vision +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, complete_task] +dependsOn: [] +--- + +## Goal + +Plan a Replay vision setup and seed the task queue. A scanner is an LLM that +watches one session recording at a time and writes an observation — a score, a +tag, or a summary — and every observation lands as a queryable PostHog event. +Scanners only see what session replay records, so the plan has two halves: +make sure recordings flow, then create scanners tailored to this product. + +First, establish one fact from the repo: **is PostHog already integrated?** +Look for `posthog-js` or a server SDK in the dependency manifests, or a +`posthog.init(...)` / snippet in the source. Check the project state for +existing events if the repo is ambiguous. + +Then seed one of two graphs: + +**PostHog is already integrated** — the common case: + +- `enable-replay`, with no dependencies. +- The three scanner tasks — `scanner-broken-experiences`, + `scanner-user-frustration`, `scanner-session-summaries` — each after + `enable-replay`, independent of one another, so they run in parallel. +- `report`, after all three scanner tasks. It writes the handoff last, so it + describes what actually shipped. + +**PostHog is not integrated** — do not stop, integrate: + +- `install` and `init`, independent of each other. +- `enable-replay`, after `install` and `init`. +- The three scanner tasks and `report`, wired exactly as above. + +Never plan an identify, capture, or dashboard task — this run sets up Replay +vision, not the full integration. The minimal SDK footprint that `install` and +`init` leave behind is enough for recordings to flow. + +## How you know you succeeded + +Every task in the chosen 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/src/programs/replay-vision/flow.ts b/src/programs/replay-vision/flow.ts new file mode 100644 index 000000000..0f1b63595 --- /dev/null +++ b/src/programs/replay-vision/flow.ts @@ -0,0 +1,21 @@ +import { defineFlow } from '@programs/program-contract'; + +/** Replay vision: recordings flow first, then three independent scanners. Install and init only when PostHog is missing. */ +export const flow = defineFlow({ + sink: 'report', + nodes: { + install: { picked: true }, + init: { picked: true }, + 'enable-replay': { dependsOn: ['install', 'init'] }, + 'scanner-broken-experiences': { dependsOn: ['enable-replay'] }, + 'scanner-user-frustration': { dependsOn: ['enable-replay'] }, + 'scanner-session-summaries': { dependsOn: ['enable-replay'] }, + report: { + dependsOn: [ + 'scanner-broken-experiences', + 'scanner-user-frustration', + 'scanner-session-summaries', + ], + }, + }, +}); diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index 26d4a07a1..f78e85513 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -25,6 +25,7 @@ import { ProgramAbort } from '@programs/program-contract'; import { ErrorCodes } from '@shared/errors'; import { REPLAY_VISION_SCOPE_ADDITIONS } from './scopes.js'; import { abortNoFrameworkDetected } from '../shared/abort-no-framework'; +import { flow } from './flow'; const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; @@ -153,6 +154,8 @@ export const config: ProgramConfig = { model: GPT6_SOL_MODEL, }, agentFlow: 'replay-vision', + agentPrompts: () => import('./agent-prompts').then((m) => m.AGENT_PROMPTS), + flow, onReady: detectReplayVisionProject, oauthScopeAdditions: REPLAY_VISION_SCOPE_ADDITIONS, From 2f89269c64f99037d2bdc45946c5dac1af5b56ed Mon Sep 17 00:00:00 2001 From: Edwin Lim Date: Thu, 29 Oct 2026 16:43:30 -0700 Subject: [PATCH 2/2] refactor(programs): import agent prompts as Markdown Each program's agent-prompts.ts imports its agent/.md files as text instead of carrying a generated copy. tsdown's .md text loader inlines them in the build, a Vitest plugin loads them in tests, and pnpm try registers a Node load hook that does the same. Co-Authored-By: Claude Opus 5.5 --- package.json | 5 +- scripts/bundle-agent-prompts.no-jest.ts | 50 ------------------- scripts/markdown-hooks.no-jest.mjs | 14 ++++++ scripts/markdown-loader.no-jest.mjs | 4 ++ src/programs/README.md | 10 ++-- src/programs/__tests__/agent-prompts.test.ts | 2 +- src/programs/error-tracking/agent-prompts.ts | 46 ++++++++--------- src/programs/feature-flags/agent-prompts.ts | 26 +++++----- src/programs/metrics/agent-prompts.ts | 19 ++++--- .../posthog-integration/agent-prompts.ts | 49 +++++++++--------- src/programs/replay-vision/agent-prompts.ts | 34 ++++++------- tsconfig.base.json | 2 +- tsdown.config.ts | 2 + types/markdown.d.ts | 5 ++ vitest.config.ts | 13 ++++- 15 files changed, 134 insertions(+), 147 deletions(-) delete mode 100644 scripts/bundle-agent-prompts.no-jest.ts create mode 100644 scripts/markdown-hooks.no-jest.mjs create mode 100644 scripts/markdown-loader.no-jest.mjs create mode 100644 types/markdown.d.ts diff --git a/package.json b/package.json index 8093c9ced..1c5fd5b66 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "test:coverage": "pnpm build && vitest run --coverage", "test:e2e": "pnpm build && ./e2e-tests/run.sh", "test:e2e-record": "export RECORD_FIXTURES=true && pnpm build && ./e2e-tests/run.sh", - "try": "tsx --tsconfig tsconfig.base.json bin.ts", + "try": "tsx --tsconfig tsconfig.base.json --import ./scripts/markdown-loader.no-jest.mjs bin.ts", "dev": "pnpm build && pnpm link --global && pnpm build:watch", "test:watch": "vitest", "prepare": "husky", @@ -157,8 +157,7 @@ "wizard-ci-replay": "tsx scripts/tui-replay.no-jest.ts", "decisions-detect": "tsx scripts/decisions-detect.no-jest.ts", "check:decisions-bundle": "tsx scripts/check-decisions-bundle.no-jest.ts", - "record:context-mill-fixtures": "tsx scripts/record-context-mill-fixtures.no-jest.ts", - "bundle:agent-prompts": "tsx scripts/bundle-agent-prompts.no-jest.ts" + "record:context-mill-fixtures": "tsx scripts/record-context-mill-fixtures.no-jest.ts" }, "lint-staged": { ".claude/settings.json": "sh -c 'printf \"\\n\\033[31mDo not commit .claude/settings.json — use .claude/settings.local.json (gitignored).\\nUnstage with: git restore --staged .claude/settings.json\\033[0m\\n\\n\" >&2 && exit 1'", diff --git a/scripts/bundle-agent-prompts.no-jest.ts b/scripts/bundle-agent-prompts.no-jest.ts deleted file mode 100644 index 6c98acd57..000000000 --- a/scripts/bundle-agent-prompts.no-jest.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Generates each program's `agent-prompts.ts` from its `agent/*.md`, so the - * bundle and `pnpm try` both carry the prompts as plain strings. Run with - * `pnpm bundle:agent-prompts` after editing a prompt; a test fails while a - * generated module is stale. - */ - -import { execFileSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const PROGRAMS = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '..', - 'src', - 'programs', -); - -export const GENERATED_HEADER = - '// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit.'; - -const written: string[] = []; -for (const program of fs.readdirSync(PROGRAMS)) { - const agentDir = path.join(PROGRAMS, program, 'agent'); - if (!fs.existsSync(agentDir)) continue; - const files = fs - .readdirSync(agentDir) - .filter((file) => file.endsWith('.md')) - .sort(); - const entries = files.map( - (file) => - ` ${JSON.stringify(file.replace(/\.md$/, ''))}: ${JSON.stringify( - fs.readFileSync(path.join(agentDir, file), 'utf8'), - )},`, - ); - const out = path.join(PROGRAMS, program, 'agent-prompts.ts'); - fs.writeFileSync( - out, - `${GENERATED_HEADER}\n\n/** This program's agent prompts by task type, verbatim. */\nexport const AGENT_PROMPTS: Readonly> = {\n${entries.join( - '\n', - )}\n};\n`, - ); - written.push(out); -} - -execFileSync('pnpm', ['exec', 'prettier', '--write', ...written], { - stdio: 'inherit', -}); -console.log(`Generated ${written.length} agent-prompt modules.`); diff --git a/scripts/markdown-hooks.no-jest.mjs b/scripts/markdown-hooks.no-jest.mjs new file mode 100644 index 000000000..ee0d0e663 --- /dev/null +++ b/scripts/markdown-hooks.no-jest.mjs @@ -0,0 +1,14 @@ +// The load hook markdown-loader.no-jest.mjs registers: a `.md` module exports the file's text. +import { readFile } from 'node:fs/promises'; + +export async function load(url, context, nextLoad) { + if (!url.startsWith('file:') || !new URL(url).pathname.endsWith('.md')) { + return nextLoad(url, context); + } + const text = await readFile(new URL(url), 'utf8'); + return { + format: 'module', + source: `export default ${JSON.stringify(text)};`, + shortCircuit: true, + }; +} diff --git a/scripts/markdown-loader.no-jest.mjs b/scripts/markdown-loader.no-jest.mjs new file mode 100644 index 000000000..9ab579568 --- /dev/null +++ b/scripts/markdown-loader.no-jest.mjs @@ -0,0 +1,4 @@ +// `pnpm try` runs from source, so it loads each `.md` import as the file's text, as the build does. +import { register } from 'node:module'; + +register('./markdown-hooks.no-jest.mjs', import.meta.url); diff --git a/src/programs/README.md b/src/programs/README.md index a6f8d7df3..ba75a82db 100644 --- a/src/programs/README.md +++ b/src/programs/README.md @@ -135,10 +135,12 @@ the node". The run's traits come from [`@services/project-traits`](../services/README.md). Each task's prompt is `agent/.md`, the job to be done; the skills it -names carry the implementation detail. `pnpm bundle:agent-prompts` regenerates -the program's `agent-prompts.ts` from that folder, which `agentPrompts` loads on -first use. Tests in [`__tests__`](__tests__) fail when the module is stale, when -a prompt differs from the recorded context-mill release in +names carry the implementation detail. The program's `agent-prompts.ts` imports +each file as text, and `agentPrompts` loads it on first use. The build inlines +the files with tsdown's `.md` loader; tests load them through a Vitest plugin, +and `pnpm try` through a Node hook, `scripts/markdown-loader.no-jest.mjs`. +Tests in [`__tests__`](__tests__) fail when a file in `agent/` isn't imported, +when a prompt differs from the recorded context-mill release in `fixtures/context-mill/`, when a graph's nodes differ from its prompts, or when a node misses the sink. diff --git a/src/programs/__tests__/agent-prompts.test.ts b/src/programs/__tests__/agent-prompts.test.ts index 64017c47f..a9e800867 100644 --- a/src/programs/__tests__/agent-prompts.test.ts +++ b/src/programs/__tests__/agent-prompts.test.ts @@ -29,7 +29,7 @@ function readPrompts(dir: string): Record { describe.each(BUNDLES)('%s agent prompts', (program, flow, config) => { const own = readPrompts(path.join(PROGRAMS, program, 'agent')); - it('bundles its agent folder exactly (run `pnpm bundle:agent-prompts`)', async () => { + it('imports every prompt in its agent folder', async () => { expect(await config.agentPrompts?.()).toEqual(own); }); diff --git a/src/programs/error-tracking/agent-prompts.ts b/src/programs/error-tracking/agent-prompts.ts index 90ec859a8..c6ef54995 100644 --- a/src/programs/error-tracking/agent-prompts.ts +++ b/src/programs/error-tracking/agent-prompts.ts @@ -1,26 +1,26 @@ -// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. +import captureExceptions from './agent/capture-exceptions.md'; +import configure from './agent/configure.md'; +import credentials from './agent/credentials.md'; +import init from './agent/init.md'; +import install from './agent/install.md'; +import linkReleases from './agent/link-releases.md'; +import report from './agent/report.md'; +import setupErrorTracking from './agent/setup-error-tracking.md'; +import testSetup from './agent/test-setup.md'; +import verify from './agent/verify.md'; +import wireCi from './agent/wire-ci.md'; -/** This program's agent prompts by task type, verbatim. */ +/** This program's agent prompts by task type: each `agent/.md`, imported as text. */ export const AGENT_PROMPTS: Readonly> = { - 'capture-exceptions': - "---\ntype: capture-exceptions\nflow: error-tracking\nlabel: Wire up exception capture\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-error-tracking-step, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nMake the errors the app does not catch reach PostHog, by whatever means the\nSDK offers for that. Which means depends on the SDK: some autocapture\nexceptions once you enable it at init, some wire into the framework's own\nerror handler, some give you a boundary to mount at the app entry. Follow the\ndocs and the reference example for this one, and set it up once per runtime —\nnever manual capture calls sprinkled across files.\n\nAn app that runs code on a server as well as in the browser has two runtimes,\nand each needs its own capture. Cover every runtime the docs cover for this\nframework and the app actually uses: the server side takes the SDK's server\nlibrary, where the framework hands you its errors (for Next.js,\n`onRequestError` in `instrumentation.ts`). A browser-only setup leaves every\nserver error uncaptured. An Electron app has the same split: its main process\nruns Node, and its uncaught errors never reach the renderer's capture, so the\nmain process takes the server library. A packaged app ships without the\ndeveloper's `.env`, so a runtime `process.env` lookup in the main process is\nalways empty there: read the token the way the bundler inlines it at build time\n(electron-vite: `import.meta.env.MAIN_VITE_*`). A fully static app has no\nserver runtime to cover.\n\nA page often shows two setups side by side: a global initializer, and an\nexplicit client you hold. Follow one of them the whole way through. Taking the\nwiring from one and the calls from the other reads fine and compiles nowhere —\nan initializer that hands back nothing leaves you nothing to call methods on.\n\nThe SDK is installed and initialized — either it already was, or the install\nand init tasks before you did it (see their handoffs); build on that, do not\nre-check it.\n\nInstall nothing except the server library your server-side capture imports,\nwhen the manifest lacks it: add it with the project's package manager\n(`detect_package_manager`). When it answers `No lockfile found. Default to\nnpm`, look up the tree first: in a workspace package, use the manager whose\nlockfile sits at the workspace root above this directory, and npm only when no\nparent holds a lockfile. Do not run tests or start the app, and do not touch\nthe build config: when the flow includes a configure task, it owns those files.\nStay inside this project's directory and set up capture in those places; that\nis the whole job.\n\nEach import needs its declaration in the manifest of the package that holds the\nimporting file. An app inside a workspace often nests its own package (a\n`v2/package.json` below this directory): when the SDK sits in a different\nmanifest than the code that imports it, add it to the importing package too,\nwith the workspace's manager. Otherwise the import resolves only through\nhoisting and breaks in a focused install or a container build.\n\nOnce your edits are in, run the project's build, or its typecheck when that\ncovers your files and is quicker, and fix what your edits broke. A failure\nthat was there before your edits is not yours: note it in your handoff and\nleave it. Only a build the project defines in this checkout counts: a package\nscript or a compiler. Most Python, Ruby, and PHP apps have none, and then\nthere is nothing to run: do not create environments, install packages, or\nreach for Docker to check your edit. Let the build check API names and types: write from the docs and\nbuild, and open an SDK's type declarations only to fix an error the build\nreports.\n\nBuild, never run: no command that installs or launches the app (`expo run:*`,\n`react-native run-*`, `flutter run`), because it opens a simulator or an\nemulator on the user's machine. Leave no build output the project does not\nignore: in Go, check with `go vet ./...`, because `go build ./...` writes a\nmain package's binary into the project root. Your shell gets a reduced\nenvironment, so a build that cannot find an SDK (`SDK location not found`)\nfailed because of this shell, not your edit: do not retry it, and say so in\nyour handoff.\n\n## Do not copy the SDK's own listeners\n\nException autocapture already registers the global handlers: `window.onerror`\nand `unhandledrejection` in the browser, `uncaughtException` and\n`unhandledRejection` in Node, `sys.excepthook` in Python, the panic hook in\nRust, and the uncaught-exception and signal handlers on iOS and Android. Turn\nthat on with the SDK's own option — `capture_exceptions`,\n`enableExceptionAutocapture`, `enable_exception_autocapture`, `capture_panics`,\n`errorTrackingConfig.autoCapture` — and register none of those handlers\nyourself. A second listener on the same event sends every error twice, and it\nstops matching the SDK's handling the moment the SDK changes.\n\nWhat the SDK cannot see is yours to add: errors a framework catches before any\nglobal handler fires. Express error middleware, Fastify `setErrorHandler`, Hono\n`onError`, Vue `app.config.errorHandler`, Angular `ErrorHandler`, SvelteKit\n`handleError`, a React error boundary, Next.js `global-error` — hook those,\nbecause the framework swallows the error and the global listener never hears\nof it. NestJS is one more layer up: its exception filters answer every request\nerror before Express error middleware runs, so capture goes in a global filter\n(the app's existing `@Catch()` filter when it has one), not in\n`setupExpressErrorHandler`. A platform with no autocapture at all, such as Go,\nis the other case: there the capture boundary at the entry point is the\nmechanism, not a copy of one.\n\n## How you know you succeeded\n\nAn error the app does not catch reaches PostHog from every runtime the app\nruns in, through the mechanism each SDK gives you rather than one you invented,\nand every PostHog library in the manifest is imported. No global error listener\nof your own sits beside the SDK's autocapture. The project builds as well as it\ndid before your edits. You installed nothing but the server library your\ncapture imports, and did not run tests, search outside the project, or read\nthrough the whole app or hand-wrap individual components or routes. Your handoff names the files you changed, the capture mechanism, and\nthe build command you ran with its result, so the report can explain it to the\nuser.\n", - configure: - "---\ntype: configure\nflow: error-tracking\nlabel: Apply build-config changes\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, check_env_keys]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture-exceptions]\n---\n\n## Goal\n\nMake this project's production build emit and upload source maps or debug\nsymbols. Flutter Android/iOS release builds upload Dart AOT symbols separately\nfrom platform-native symbols. Install the skill your task input names\n(`install_skill` with the `skillId`) and read it — it is the source of truth\nfor the per-framework build-config and the uploader wiring.\n\nTwo of the skill's steps are yours:\n\n- **\"Apply build-config changes\"** — make the bundler / build-config edits the\n skill instructs for this platform, so the build produces matching maps or\n debug symbols and runs the uploader. Only web source maps need chunk IDs.\n- **\"Make credentials available at build time\"** — do the skill's step so the\n build can read the upload credentials from the environment. If it calls for a\n loader (e.g. `dotenv`), install it SILENTLY with the project's package\n manager. Skip this step entirely when the platform already auto-loads `.env`.\n\nInstall every dependency with the project's own package manager: call\n`detect_package_manager` before the first install and use its answer,\ntranslating any `npm install` the skill or docs show (`pnpm add -D …`,\n`yarn add -D …`). In a pnpm or yarn workspace, npm fails outright on\n`workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) — that error means the\nwrong manager, never a flag to retry with. When `detect_package_manager`\nanswers `No lockfile found. Default to npm`, look up the tree first: in a\nworkspace package, use the manager whose lockfile sits at the workspace root\nabove this directory, and npm only when no parent holds a lockfile.\n\nChange only what the upload needs. Do not remove or weaken a source-map setting\nthe project already has (`productionBrowserSourceMaps`, Vite's\n`build.sourcemap`, webpack's `devtool`): a build without the upload credentials\nmust still emit what it emitted before. Do not upgrade a package the project\nalready depends on to quiet a peer-dependency warning, because a warning is not\na failure. Upgrade one only when the install or build fails on it, or when the\nskill requires a newer version for the feature you are wiring. Keep the\nproject's version style: an exact pin stays exact.\n\nWire the upload into a build the project already has, in this directory or in\na package nested below it. Code that no build compiles or bundles runs as\nwritten, and its stack traces are already readable: do not add a bundler, a\ncompiler, or a new `build` or `start` script to create one. When this project\nhas no production build to attach an upload to, report this task as\n`not needed` and say why in your handoff.\n\nWhen the upload step you wire runs a machine-global `posthog-cli` rather than\none from the project's own dependencies, do not try to install it or check for\nit: a global install, `posthog-cli` itself, and `command -v` are blocked for\nyou, and the wizard already tried the install before your task. Name it in\nyour handoff as a follow-up for every other machine that runs the release\nbuild — `npm install -g @posthog/cli@latest` — because without it the upload\nstep cannot run.\n\nWhen your changes make the build emit a bundle to a new directory (`dist/`,\n`build/`), check that some script actually runs that output. A project whose\n`build` writes `dist/index.js` while `start` still runs the original source\nnever executes the bundle the maps were uploaded for, so every uploaded map\ngoes unused. Add or fix the script that serves the built output.\n\nThat run script has its own environment to satisfy, and it is not the build's.\nThe uploader's credentials are handled — your build step passes them itself. The\nseparate question is the variables **the application reads when it starts**: the\nproject token and host its init looks up. Those live in the same gitignored env\nfile, and a compiled binary or a bare interpreter loads nothing on its own.\n\nSo open the init point and note the variables it reads. When the project\nalready loads the env file for them, you're done: never remove an env-file\nflag or loader the project's `start` already has, because that local loading\nis the project's choice. When it doesn't, do not wire\na loader into the run script: production supplies these as real environment\nvariables, and a script that loads the gitignored file breaks wherever it is\nmissing. Never put a hard `--env-file=.env` on `start`. Leave the script as it\nis and name the gap in your handoff as a manual follow-up, with the variable\nnames. Leave it out of the handoff and the build is green, the symbols are\nuploaded, and the single command you hand the user to verify with is the one\ncommand that cannot capture.\n\nPut a multi-step build somewhere the tool actually runs it — a script file, a\nmake target, the manifest's own scripts — never an alias mechanism you are\nassuming exists, borrowed from a neighbouring tool.\n\nMatch the bundle's module format to the package's type. A build passes either\nway, and the mismatch only shows when the bundle starts: `esbuild\n--platform=node` emits CommonJS unless you pass `--format=esm`, so in a package\nwhose `package.json` sets `\"type\": \"module\"` the bundle dies at boot with\n`ReferenceError: module is not defined in ES module scope`. Read the `type`\nfield before writing the command.\n\n## The names are a contract\n\nAn env variable only works if the name the code reads is the name in the file.\nYou write the code that reads them; the `credentials` task writes the file, and\nyou two run in parallel. So never invent a name that already exists somewhere\nelse — look for the other half of the contract first, and adopt it:\n\n- **Call `check_env_keys` before you write the config.** It returns names, never\n values. If the PostHog upload variables are already there, make your config\n read exactly those names, whatever they are, even when they are not the ones\n your skill's example shows.\n- **Only when they are absent yet** are you the one deciding. Use the names your\n skill documents for the mechanism you are wiring, and name them in your\n handoff in full so `credentials` and `wire-ci` can match them.\n\nNames that merely look plausible are the failure here. A build reading\n`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no\nerror anywhere — the upload is skipped silently, the build looks clean, and\nevery stack trace stays minified.\n\nDo not write any credential values and do not create env files — the\n`credentials` task owns that, in parallel with you.\n\n## Build it\n\nOnce your edits are in, run the production build and read its exit status, not\nonly its log: the upload often runs before the type check, so the maps can\nland in a build that still fails. Fix what your changes broke and build again.\nA failure that was there before your changes is not yours: note it in your\nhandoff and leave it. Let the build check option names and types: write from\nyour skill, build, and open a plugin's type declarations only to fix an error\nthe build reports.\n\nBuild, never run. A command that installs or launches the app (`expo run:*`,\n`react-native run-*`, `flutter run`, `./gradlew install*`) opens a simulator\nor an emulator on the user's machine, even where your skill names it as the\nbuild. On an Expo app whose `android/` and `ios/` folders are generated, build\nthe JavaScript bundle instead: `npx expo export --source-maps --platform\n`. Your shell blocks `make`, shell scripts, and a Gradle wrapper\noutside the project root: when you put the build in a `make` target or a\nscript, run the build command inside it directly. Never add a package script\nto get past a blocked command. Skip it, and say in your handoff what you could\nnot run.\n\nYour shell gets a reduced environment: `ANDROID_HOME` and other variables from\nthe user's shell do not reach it. A build that cannot find an SDK (`SDK\nlocation not found`) failed because of this shell, not the project. Do not\nretry it, and do not give the user a setup step for it: say the build could\nnot run here.\n\nThe `credentials` task may not have written the upload credentials yet, so this\nbuild may run without them. It has to pass either way. A teammate's checkout\nand a pull-request build have no key either, and a config that demands one\nstops their builds. When the build fails only because the upload credentials\nare missing, make the upload skip itself when they are absent, with the option\nyour skill's plugin or CLI offers, and build again. Once the credentials are\nwritten you cannot build without them: the build reads the env file, and a\nprefix such as `VAR= npm run build` is blocked. Check the skip path by reading\nthe config instead.\n\nWhen the credentials are in place, the build creates a release and uploads to\nthe user's project. That is expected; it is what every production build will\ndo.\n\n## How you know you succeeded\n\nThe build config carries the skill's source-map / debug-symbol changes and can\nread its credentials from the environment at build time. The production build\npasses with your changes, with or without the upload credentials. Your handoff\nnames every file you changed, the exact build-config keys you added, and the\nbuild command you ran with its result and whether the credentials were\npresent, so the CI task can wire the same variables through the pipeline.\n", - credentials: - '---\ntype: credentials\nflow: error-tracking\nlabel: Get and write the upload credentials\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nPut the PostHog source-map upload credentials into this project\'s environment.\nInstall the skill your task input names (`install_skill` with the `skillId`)\nand follow its **"Write credentials to the env file"** step for the variable\nnames and the env file to pick.\n\nThe upload needs a PostHog **personal API key** at build time. Only the user can\nmint one — never call the PostHog API or any tool to create it. Get it and\nwrite it in this one task (the key never survives across tasks):\n\n1. Ask with `wizard_ask`, exactly:\n `{ id: "api-key", prompt: "Uploading source maps needs a PostHog personal API key. Source maps let PostHog show your original code in error stack traces, not minified code.\\n\\nCreate a key with the right permissions here:\\n/source-maps-key\\n\\nThen paste it below.", kind: "text", sensitive: true }`\n You receive `{ secretRef: "secret:..." }` — a vaulted reference, never the raw\n value. If `wizard_ask` is unavailable (non-interactive run), report this task\n with status `not needed` and say in your handoff that the user must create\n the key and set the variables themselves; do not block.\n2. Pick the env file per the skill (reuse the one PostHog\'s SDK already writes\n its `POSTHOG_*` vars to, when there is one). Call `check_env_keys` on it\n first (it returns present/absent, never values — never read the file\n directly).\n3. Settle the variable names before you write them — see "The names are a\n contract" below. Then call `set_env_values`, passing the secretRef as a\n value object, not a literal string — e.g.\n `values: { "POSTHOG_CLI_API_KEY": { secretRef: "" }, "POSTHOG_CLI_PROJECT_ID": "", "POSTHOG_CLI_HOST": "" }`\n with whichever names you settled on. The wizard resolves the ref locally, so\n you never see the key value.\n4. Document the same variable names for other developers: append them to\n `.env.example` (create it if the project has none) with empty or\n placeholder values — never a real value, and never the key itself. The\n example file is committed and is the only `.env*` you may write directly;\n it is how the next developer, and the next wizard run\'s `check_env_keys`,\n learns the project expects these variables. The `init` task may write the\n same file beside you, so read it right before you change it and add your\n lines with `edit`; never `write` over it once it exists.\n\nReplace `` and `` from your project context. Do not touch the build config — the `configure` task owns that.\n\n## The names are a contract\n\nAn env variable only works if the name in the file is the name the code reads.\nYou write the file; the `configure` task writes the code that reads it, and you\ntwo run in parallel. So never invent a name that already exists somewhere else\n— look for the other half of the contract first, and adopt it:\n\n- **Read the build config before you choose.** If it already references PostHog\n env variables, use exactly those names, whatever they are, even when they are\n not the ones your skill would suggest. Reading config is safe — it holds\n names, not secrets.\n- **Only when nothing references them yet** are you the one deciding. Use the\n names your skill documents for the mechanism that was actually wired.\n- **Either way, say which set you chose in your handoff, in full.** That is how\n `configure` and `wire-ci` learn what to match. A handoff that says "the usual\n variables" hands the next task the same guess you just made.\n\nNames that merely look plausible are the failure here. A build reading\n`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no\nerror anywhere — the upload simply never runs, and every stack trace stays\nminified.\n\n## How you know you succeeded\n\nThe env file holds the upload variables (the key as a resolved secret, the\nnon-secret project id and host as literals), written through the wizard tools,\nnever hardcoded in source, and `.env.example` documents the same names with\nplaceholders. Your handoff names the env file and every variable name — never\na value — so the CI task carries the same names into the pipeline.\n', - init: "---\ntype: init\nflow: error-tracking\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, set_env_values]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure PostHog is initialized. If the project already has a working\n`posthog.init(...)` (or the framework's equivalent) with its env keys wired,\nleave it alone and say so in your handoff. If it doesn't, create it following\nyour skill — it owns the how: the framework's init point, the env-var wiring\nthrough the wizard tools, and `.env.example`. The `credentials` task may write\n`.env.example` beside you, so read it right before you change it and add your\nlines with `edit`; never `write` over it once it exists, or the upload\nvariables it documented are lost.\n\nYou exist in this flow because the user asked for error tracking on a repo\nwhose PostHog init is missing or unproven. Initialize the SDK so exceptions can\nflow and stop — no instrumentation, no extras. Don't set up exception capture\neither way; the capture-exceptions task after you owns that.\n\n## Initialize the server the project ships\n\nA directory can hold more than one server: a workspace often keeps a small\nscript beside a nested package with its own manifest (`index.js` next to\n`v2/package.json`). Initialize each server the project builds or deploys, the\none with a `build` or `start` script, a Dockerfile, or a job in the pipeline. A\nscript the project only runs in development, with nothing but a `dev` script,\ngets no client. Name the server and its entry file in your handoff, so the\ntasks after you cover the same one.\n\n## An existing init still needs its variable defined\n\n\"Already initialised\" is a property of the pair, not of the call. An init point\nthat reads `process.env.SOMETHING` only works when `SOMETHING` is defined in the\nenv file the project loads. A repo can carry an init that has never once run:\nthe call is there, the variable it names is nowhere, and the client is built\nfrom an empty string.\n\nSo before you leave an existing init alone, call `check_env_keys` on that env\nfile and look for the exact name the init reads. It returns names, never values.\n\n- **Present** — the pair is complete. Leave it alone and say so.\n- **Absent** — the init is not wired yet, whoever wrote it. Write that variable\n with `set_env_values` under the name the code already reads, and document it\n in `.env.example`. Do not rename the code to match a name you would rather\n have written; the code is the half that already exists.\n\nAn empty key is the quiet failure here. A client constructed from `''` throws\nnothing and logs nothing. The build is clean, the app starts, every capture call\nreturns — and no event ever arrives.\n\n## Make the environment actually reachable\n\nWriting the keys to `.env` is only half the job — something has to load that\nfile at runtime, or the app throws on boot and captures nothing.\n\nMost frameworks do it for you: Next, Nuxt, Astro and SvelteKit auto-load `.env`,\nand Vite auto-loads it for client code. Nothing to do there.\n\nA plain Node backend does not — Express, Fastify, Hono, Koa, a raw `node:http`\nserver — and neither does a bare Rollup or webpack config. When the project\nalready loads `.env` there (a `dotenv` import, an env-file flag in its scripts),\nleave it alone.\n\nWhen nothing loads it, do not wire a loader. Don't install a package for it and\ndon't edit `start` or `dev`. Every way in is a guess about where the app runs: a\nhard `--env-file=.env` stops the app wherever the gitignored file is missing,\n`--env-file-if-exists` needs Node 22.9, and putting the variables in front of\nthe command commits the key. Name it in your handoff as a manual follow-up\ninstead, with the variable names and the user's two options: load `.env` with\n`dotenv`, or set the variables in the app's environment. Say that until then,\nthe guard stops the app at boot in development with\n`POSTHOG_… variable required by PostHog is missing or un-configured`. That loud\nfailure is the guard doing its job.\n\nSome platforms have no environment to read at all, and there the answer is not\na loader. Angular on the stock `@angular/build` builder is the common one:\nnothing populates `process.env`, `import.meta.env`, or the project's own\n`src/environments/.env.ts` with your keys.\n\nThe trap is that each of those *looks* like a mechanism. `import.meta.env` and\nthe `NG_APP_` prefix only exist with `@ngx-env/builder` installed, and a\ngenerated `.env.ts` usually carries one unrelated key such as\n`npm_package_version` and nothing else. Read from either and your key is\n`undefined`: in development the guard throws while the module evaluates and the\napp renders a blank page, and in a production build the guard returns quietly,\nso the app looks fine while PostHog never initialises at all.\n\nSo do not wire a lookup unless you have opened the thing it reads from and seen\nyour key defined there. When nothing populates it, write the real public project\ntoken as a literal in the committed `src/environments/*` files. This is the\nskill's \"no valid environment to read from\" case, and the public token is\npublishable — it ships inside the browser bundle either way.\n\n## Leave the build to later tasks\n\nThe `install` task may still be adding the SDK beside you, so do not build\nhere: the build would fail on a package that is about to arrive. The tasks\nafter you build the project once every edit is in, and fix what does not\ncompile.\n\n## How you know you succeeded\n\nAn init point exists with the PostHog env keys present — whether it already\ndid or you just created it — keys in the env file and confirmed there with\n`check_env_keys`, never hardcoded. On a platform that does not auto-load\n`.env` and has no loader, your handoff names it as a manual follow-up; on a platform with no environment at all, the\ntoken is a literal rather than a lookup into something that never defines it.\nYour handoff names the files involved, how the client is constructed, and how\n`.env` reaches it, so the capture-exceptions task can find the init options\nwithout re-discovering them.\n", - install: - "---\ntype: install\nflow: error-tracking\nlabel: Add the PostHog SDK to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-haiku-4-5\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure the PostHog SDK is in the manifest. If it's already installed,\nleave it alone and say so in your handoff. If it isn't, install it following\nyour skill — it owns the version rules, what counts as an environment failure,\nand the fallback. The package manager comes from the section below, which wins\nover your skill's own rule: the skill looks at this directory only.\n\nYou only exist in this flow because the user asked for error tracking on a\nrepo without PostHog. Install the SDK the init will construct — the browser\nSDK for an app with a browser side, the server SDK for a server-only app — and\nstop: no instrumentation, no extras. A second, server-side library for an app\nthat has both is not yours to add. The `capture-exceptions` task adds it when\nit wires server-side capture, so the manifest never carries a library no code\nimports.\n\n## Pick the package manager first\n\nCall `detect_package_manager`. When it names a manager, use it. When it answers\n`No lockfile found. Default to npm`, do not run npm yet: this directory may be\na package inside a workspace, whose lockfile sits at the repository root. Read\n`../package.json` and `../../package.json` up to the root, and look there for a\nlockfile (`pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `package-lock.json`) or a\n`packageManager` field. Reading them is part of this task even though they sit\nabove this directory; edit nothing there. Use the manager they name, from this\ndirectory. npm is right only when no parent holds either: npm in a pnpm or Yarn\nworkspace fails on `workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) or\nleaves a second lockfile behind.\n\n## Python\n\nIn a Python project, install into the virtualenv the project already keeps in\nits directory. When it has none, add the package to the manifest and stop. Do\nnot create one: it would stay behind in the user's project.\n\n## How you know you succeeded\n\nThe SDK is declared in the manifest at a real version — whether it already\nwas or you just installed it — or your handoff plainly says why the\nenvironment stopped you. Your handoff names the manifest and the package, so\nlater steps import it under the name they will actually get.\n", - 'link-releases': - "---\ntype: link-releases\nflow: error-tracking\nlabel: Link deploys to releases\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, detect_package_manager]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture-exceptions]\n---\n\n## Goal\n\nLink every exception this app sends to the production release it came from.\nThis platform ships its source as is, so there is nothing to upload. Instead the\nproduction deploy resolves a PostHog release, and the app starts with that\nrelease's ID in `POSTHOG_RELEASE_ID`. Install the skill your task input names\n(`install_skill` with the `skillId`) and follow it. It owns the minimum SDK\nversion, the CI step per provider, and how the ID crosses each deploy boundary.\n\nYour task has three parts, in this order:\n\n1. **SDK version.** Make sure the project resolves at least the SDK version\n that reads `POSTHOG_RELEASE_ID`, per the skill's \"Update the SDK\" step. An\n older SDK ignores the variable without an error. Refresh the lock for that\n one package with the project's own tool: call `detect_package_manager`\n first. Do not upgrade across a major version when code from before this run\n already calls the SDK; when the `install` handoff says this run added the\n SDK, raise it straight to the minimum. The production runtime\n decides what can install, not the interpreter on this machine: never lower\n the version below the minimum because a local install fails.\n2. **Resolve the release in the production deploy.** Trace the deploy path by\n reading the project's own files: CI workflows, Dockerfiles, compose files,\n deploy scripts, service and pool configs. Add the resolve step to the job\n that deploys to production, and to no other job.\n3. **Get the ID into the running app.** Pass the ID on at every boundary\n between that step and the process that creates the PostHog client, until\n that process starts with `POSTHOG_RELEASE_ID` in its environment.\n\n## Local development stays as it is\n\nThis is a deploy-time change only. Local runs, tests, pull-request builds, and\npreviews send no release ID, and that is correct. So do not touch run scripts,\ndev servers, test config, `.env`, or `.env.example`. Do not ask for a personal\nAPI key, because nothing local uses one. Never run `posthog-cli` yourself: a\nresolve creates a real release in the user's project.\n\nOnly edit CI and deploy files that already exist. When this project has no\ndeploy path you can open and follow, make no CI changes. Report the task done\nwith the SDK update alone, and put the requirement into your handoff as a\nmanual follow-up.\n\n## The last hop decides it\n\nA release ID that stops one boundary short of the app looks correct in the\ndiff and does nothing. The resolve step succeeds, the deploy is green, and\nevery exception still arrives without a release. Each of these boundaries drops\nthe value unless you pass it on: a `docker build`, a container start, an SSH\nsession, a service manager's environment, PHP-FPM's `clear_env`, and Laravel's\ncached config. So walk the path to its end, and pass the value on the same way\nthe deploy already passes the app's other runtime variables.\n\nIn a workspace package, the deploy path usually starts at the repository root,\nabove this directory (`../.github/workflows`, a root `Dockerfile`): look there\nbefore you conclude there is none. When you cannot open those files, name them\nin your handoff.\n\nWhile you are at the start step, check the other names on it too. The app's\nPostHog init reads a token and a host, and the start step may predate PostHog.\nIf it passes the app's variables by hand and leaves those out, add them by the\nnames the app's source reads, and name any secret the user must create.\n\n## How you know you succeeded\n\nThe project resolves an SDK version that reads `POSTHOG_RELEASE_ID`, or your\nhandoff names the upgrade as a follow-up. The production deploy job resolves\nthe release before it ships, without failing the deploy when PostHog cannot be\nreached or a PostHog secret does not exist yet. The user merges this change\nbefore creating the secrets, so never stop a deploy step on an empty PostHog\nvariable (`${VAR:?…}`, an `exit 1`). The ID reaches the app process as\n`POSTHOG_RELEASE_ID`, and no local, test, or pull-request path changed. Your\nhandoff lists the files you changed, the SDK version before and after, the\ndeploy path hop by hop, and every secret and variable the user must create,\nwith its exact name and where it goes.\n", - report: - "---\ntype: report\nflow: error-tracking\nlabel: Summarise and hand off\nmodel_pi: openai/gpt-5.6-luna\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Glob, Grep, Write]\ndisallowedTools: [enqueue_task, publish_handoff]\ndependsOn: [capture-exceptions, wire-ci, link-releases, verify]\n---\n\n## Goal\n\nTell the user what error tracking now does for them and what they still have\nto do, from the handoffs of every task in the run. `read_handoffs` gives you\neach task's report — the capture mechanism, the files changed, the env\nvariable names, the CI secrets and variables to create, and any deploy path\nthat could not be traced. Do not re-derive any of it from the project.\n\nThis flow keeps its report on disk. Write it once, with `write`, to\n`posthog-error-tracking-report.md` at the top level of this project's\ndirectory: the wizard's closing message sends the user to that file and reads\nits warning block, and `test-setup` appends to it. `publish_handoff` is\nblocked for this task; the file is the report. When any handoff carries a\nmanual follow-up, START\nthe report, right under its H1, with this warning block, one short bullet per\nitem. The wizard shows these bullets again when it exits.\n\n```markdown\n> ⚠️ **Needs your attention**\n> - Load `.env` in the app, or set `POSTHOG_API_KEY` in its environment.\n```\n\nThen, when the run wired source-map upload or release linking, or any\nfollow-up has steps, add a **\"What you still need to do\"** section — numbered,\ncopy-pasteable:\n\n1. Create a personal API key at `/source-maps-key` (skip when the\n credentials handoff says the key is already written and no release linking\n was wired). The link opens the key form with the 'Source map upload' preset\n selected; the preset carries the error tracking write scope that both\n uploads and release linking need.\n2. Add it as the CI secret the wire-ci or link-releases step referenced, named\n exactly as in the pipeline config. When link-releases also reads a project\n ID variable (such as `POSTHOG_PROJECT_ID`), add it with the value\n ``, and say where each goes in the CI provider's settings.\n3. Any other manual follow-up the handoffs carry (an env file nothing loads,\n an untraceable deploy path, provider-side settings, installing a missing\n `posthog-cli`, an SDK upgrade across a major version).\n\nThen cover, briefly and concretely:\n\n- How uncaught errors reach PostHog now — the capture mechanism and the files\n that carry it.\n- If the run also installed and initialized the SDK, say so — the user\n started this command without PostHog and now has it.\n- When source-map upload was wired and neither the credentials nor the wire-ci\n handoff reports a skip: the files changed (paths only), the exact production\n build command, and that every production build now uploads.\n- When the build was wired but the credentials or wire-ci handoff reports a\n skip: the same files and command, and that the build is set up to upload.\n Say that uploads start only once the upload variables are present where the\n build runs.\n- What the `verify` handoff found: the build command it ran and whether it\n passed. When it uploaded, name the release and how many files went up, so\n the user knows the first symbol set is already in PostHog. Anything still\n failing is a follow-up in the warning block. When the build could not run in\n the wizard's shell (an SDK it could not find, a blocked command), the\n follow-up is to run that build command once themselves, not to set anything\n up: their own shell has what the wizard's lacked.\n- When it was skipped: one line saying why — readable stack traces on this\n platform, or that Astro is not supported by the uploader. An outcome, not an\n apology. Say plainly that the build command was left untouched.\n- When release linking was wired: the files changed (paths only), the SDK\n version it now requires, and the deploy path the release ID takes to the\n app. Say that each production deploy resolves a release and starts the app\n with `POSTHOG_RELEASE_ID`, so every exception names the release, version, and\n commit it came from. Say plainly that local runs, tests, and pull-request\n builds are unchanged and send no release ID. When the link-releases handoff\n reports no deploy path it could follow, say what the user's deploy must do\n instead.\n- How to verify: trigger any error and look at\n `/project//error_tracking`; uploaded symbol sets appear\n at `/project//error_tracking/configuration`. With\n release linking, check after the next production deploy that the stack trace\n shows the release, and that the exception's `$release_id` matches the ID the\n deploy resolved.\n\nNever write a secret value into the report — only variable names. Replace\n`` and `` from your project context. Give the same\nsummary in chat, warning block first.\n\n## How you know you succeeded\n\n`posthog-error-tracking-report.md` exists on disk, written in one `write`\ncall, and a user who reads only the file knows\nhow errors reach PostHog, whether the build passed with the changes, the\nfollow-ups they still owe (the API key and the\nCI secrets and variables named exactly, when upload or release linking was\nwired), and where in PostHog to see the first captured exception.\n", - 'setup-error-tracking': - '---\ntype: setup-error-tracking\nflow: error-tracking\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan a PostHog Error Tracking setup and seed the task queue. The end state:\nerrors the app does not catch reach PostHog; where the platform ships minified\nbundles or stripped binaries, production builds upload the source maps or debug\nsymbols that make the stack traces readable; and where the platform runs its\nsource as is (Python, Ruby, PHP), each production deploy links its errors to a\nrelease.\n\nFirst establish three facts from the repo:\n\n**1. Is PostHog already integrated?** Look for `posthog-js` or a server SDK in\nthe dependency manifests, or a `posthog.init(...)` / snippet in the source.\nCheck the project state for existing events if the repo is ambiguous.\n\nIntegrated means the init runs, not that the package is listed. An init point\nis a pair: the call, and the key it is constructed from. A repo can carry the\ncall while the key it names is defined nowhere — then the client is built from\nan empty string and captures nothing, however complete the manifest looks. So\nwhen you find an init that reads a variable, look for that same name in the\nrepo\'s committed env template or its build config. Found, or the project state\nshows real events arriving: integrated. Named nowhere, or you cannot tell:\n**queue `init`**. It re-checks the pair itself and leaves a complete init\nalone, so queuing it when you are unsure costs one cheap task, while skipping\nit on a keyless init costs the whole run.\n\n**2. Which uploader variant is this project — or none?** Read the manifests\nand pick at most one, by this precedence (first match wins):\n\n- `react-native` or `expo` in `package.json` dependencies → `react-native`.\n A React Native repo also carries `ios/Podfile`, an `.xcodeproj` and Gradle\n files for its native shells, so this rule wins over every native marker\n below. Picking `ios` or `android` for it uploads no JavaScript source maps,\n and every JavaScript stack trace stays minified.\n- `pubspec.yaml` → `flutter`\n- an `.xcodeproj`, `Podfile`, or `Package.swift` → `ios`\n- a Gradle build file that applies `com.android.application` or\n `com.android.library`, or an `AndroidManifest.xml` → `android`. A Gradle\n project without these markers is a JVM server: keep reading.\n- `go.mod` → `go`\n- `Cargo.toml` → `rust`\n- `astro` in `package.json` dependencies → **none**. Astro is not supported by\n the uploader: it inlines scripts below its asset limit into the HTML, so a\n build routinely emits a `.map` with no `.js` beside it, and the upload step\n then fails the whole build. This rule wins over every `package.json` match\n below — an Astro project that also depends on `vite` is still **none**.\n- otherwise read `package.json` dependencies, first match wins:\n `nuxt` → `nuxt`; `next` → `nextjs`;\n `@angular/core` → `angular`; `vite` → `vite`; `webpack` → `webpack`;\n `rollup` → `rollup`; `react` → `react`; server-only Node → `node`;\n any other browser JS → `web`. Server-only Node is `node` only when a\n build compiles or bundles it (a `build` script running `tsc`, esbuild, or\n another bundler), in this directory or in a package nested below it.\n Server JavaScript that no build touches runs as written: **none**.\n- **none** for platforms whose stack traces are already readable: plain\n Python (Django, Flask, FastAPI), Ruby, PHP, Elixir, JVM servers, .NET.\n Skip the whole upload subgraph for them — a skipped upload on such a\n platform is an outcome, not a gap. Fact 3 decides what Python, Ruby, and\n PHP get instead.\n\nWhen a variant matched, the uploader skill id is\n`error-tracking-upload-source-maps-`. Pass it to the four upload\ntasks as `inputs: { skillId: "", displayName: "" }`\nso no task re-detects.\n\n**3. Which release-linking variant is this project — or none?** It applies\nwhen the app itself is a server that runs Python, Ruby, or PHP source as is:\n\n- a Python app (`requirements*.txt`, `pyproject.toml`, `Pipfile`, or\n `setup.py`; Django, Flask, FastAPI, and plain Python alike) → `python`\n- a Ruby app (`Gemfile` or a `*.gemspec`; Rails, Sinatra, and plain Ruby\n alike) → `ruby`\n- a PHP app (`composer.json`; Laravel and plain PHP alike) → `php`\n- **none** for every other platform. A manifest that only carries tooling for\n another platform does not count: a `Gemfile` for CocoaPods or fastlane beside\n a mobile app, or a `requirements.txt` for scripts beside a Node service. The\n mobile uploader variants (`react-native`, `flutter`, `ios`, `android`) always\n mean none here. Bundled and compiled platforms get their release from the\n uploader, and Elixir, JVM servers, and .NET have no release-linking skill\n yet.\n\nWhen a variant matched, the skill id is\n`error-tracking-link-releases-`. Pass it to `link-releases` as\n`inputs: { skillId: "", displayName: "" }`. Fact 3\ndoes not depend on fact 2: a Django app that also bundles a Vite frontend in\nthe same project gets both the upload subgraph and `link-releases`, and both\ndefault to the same release.\n\nThe three facts are independent — settle ALL of them before you enqueue\nanything. "PostHog is already integrated" answers fact 1 only; it never decides\nfact 2 or fact 3, and an already-integrated project still gets the upload\nsubgraph or `link-releases` when a variant matches. A compiled or bundled JS\nproject normally has an uploader variant: a Node service built with `tsc` ships\nminified/compiled output, so it is the `node` variant, not "none". Only three\nkinds of project skip the upload subgraph — the readable-stack platforms listed\nabove, Astro, and server JavaScript that no build compiles or bundles.\n\nThen seed the graph:\n\n- `install`, only when the SDK is missing from the manifest.\n- `init`, independent of `install` — whenever fact 1 did not show a complete\n pair. Do not stop on an uninstrumented repo, integrate.\n- `capture-exceptions`, after whichever of `install` and `init` you queued\n (with no dependencies when you queued neither).\n- When an uploader variant matched, add the upload subgraph:\n - `credentials`, no dependencies — it stops to ask the user for a personal\n API key, so keep it a root task: the prompt reaches the user early while\n the code tasks run.\n - `configure`, after `capture-exceptions` — build-config changes; it runs\n after the code edits so the two never fight over the same files.\n - `wire-ci`, after `configure` and `credentials`.\n- When a release-linking variant matched, `link-releases`, after\n `capture-exceptions` — and after `wire-ci` too when you queued the upload\n subgraph, because both edit the deploy pipeline. It changes production\n deploys only, needs nothing from the user during the run, and leaves local\n development as it is.\n- When you queued the upload subgraph, `verify`, after every other task that\n edits code: `capture-exceptions`, `configure`, `credentials`, and\n `link-releases` when you queued it. It builds the project once every edit is\n in, with the upload credentials in place, and fixes what broke. It does not\n wait for `wire-ci`: that task edits only CI files, which a local build never\n reads, so the two run side by side. A project without the upload subgraph\n has no build of its own to check (Python, Ruby, and PHP apps build only in\n their deploy), so it gets no `verify`.\n- `report`, after every other queued task except `test-setup`. It writes the\n handoff once the work is done, so it describes what actually shipped.\n- When you queued the upload subgraph, `test-setup` last, after `report` — it\n offers the user an optional local end-to-end test. The report comes first on\n purpose: a user who walks away still gets it.\n\nA task can name another as a dependency only once that one is queued and has\nan id. So queue the tail one call per turn, never batched: `verify` when you\nqueue it, then `report` naming the `verify` id, then `test-setup` naming the\n`report` id. A `report` queued in the same turn as `verify` cannot wait for it,\nand then reports a build that has not finished.\n\nNever plan an identify, capture, dashboard, or session-replay task — this run\nsets up error tracking, not the full integration. The minimal SDK footprint\nthat `install` and `init` leave behind is enough for exceptions to flow.\n\n## How you know you succeeded\n\nEvery task in the chosen graph is queued with that dependency shape, the four\nupload tasks (when queued) share the same `{ skillId, displayName }` inputs,\n`verify` (when queued) depends on every task that edits code except `wire-ci`,\n`report` depends on every other task except `test-setup` (directly or\ntransitively), `test-setup` (when queued) depends on `report`, and the first\ntask is runnable. `link-releases` (when queued) carries its own\n`{ skillId, displayName }` inputs. Your plan states all three facts explicitly:\nwhether PostHog was integrated — and, when you called it integrated, the name\nof the key you found defined —, which uploader variant matched — or, when you\nqueue no upload tasks, why no variant applies: which readable-stack platform\nthis is, or that Astro is not supported by the uploader —, and which\nrelease-linking variant matched, or that none applies. A plan that never\nmentions fact 2 or fact 3 is an incomplete plan, not a decision. Keep labels\nshort — the action in a few words.\n', - 'test-setup': - '---\ntype: test-setup\nflow: error-tracking\nlabel: Offer an optional local test\nsink: true\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask]\ndisallowedTools: [enqueue_task]\ndependsOn: [report]\n---\n\n## Goal\n\nThe setup is done and the report is written. Offer the user an optional,\none-time, end-to-end check that errors reach PostHog with readable stack traces.\n\nYou run after the report on purpose. A user who stepped away still gets a\nfinished setup and a report, so nothing waits on this test.\n\nAsk before you do anything else, with `wizard_ask`:\n`{ id: "test-affordance", prompt: "Error tracking is set up, and the report is in posthog-error-tracking-report.md. Optional: want me to help you test it locally? I\'ll add a temporary test button (or route) and build the app, so you can run it and confirm errors show up in Error Tracking with readable stack traces. Then I\'ll remove it.", kind: "single", options: [{ label: "Yes, help me test it", value: "yes" }, { label: "No, I\'m done", value: "no" }] }`\n\n- **"no"**, no answer (`__cancelled__`), or `wizard_ask` unavailable: report\n this task done right away, noting the test was offered and not taken. Install\n no skill and read nothing first.\n- **"yes"**: install the skill your task input names (`install_skill` with the\n `skillId`) and follow its **"Test the local setup"** step for the\n platform-appropriate affordance, the `captureException` shape, the\n placement, and the read-before-edit / always-revert rules. Add the\n affordance, then run the production build yourself: it uploads the maps for\n the bundle that carries the affordance.\n When the build fails, revert, tell the user what failed, and stop. When it\n passes, pause with a second `wizard_ask` (id `"test-done"`, a single\n `Continue (revert test code)` option) whose prompt gives the rest as literal\n numbered steps: run the built app, trigger the affordance, then confirm the\n error resolves to real source in Error Tracking. When the platform\'s build\n and run are one step (an Xcode Run, `expo run`), give that step to the user\n instead of building. After the user continues, or when that ask comes back\n unanswered, REVERT every test edit per the skill\'s rules. Never leave the\n affordance in place, even if the user says it didn\'t work.\n\nWhen the user says the test failed, add a short **"Local test"** section at the\nend of `posthog-error-tracking-report.md` with what they saw, and say the same\nin chat. The report task has already run, so this is the only place the\nfailure is written down.\n\n## How you know you succeeded\n\nEither the offer was declined or unanswered and no code changed, or the\naffordance was added, tested, and fully reverted. Your handoff says which, and\na failure the user reported is in the report file.\n', - verify: - "---\ntype: verify\nflow: error-tracking\nlabel: Build and check the setup\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, detect_package_manager]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture-exceptions, configure, credentials, link-releases]\n---\n\n## Goal\n\nEvery edit of this run is in. Prove the setup works by building the project\nthe way it builds for production. `read_handoffs` names the files each task\nchanged, the packages it added, and the env variable names it used.\n\nRead the handoffs first. When the `configure` handoff reports a passing\nproduction build that ran with the upload credentials present, and this run\nhas no `link-releases` task, that build already ran on the finished code: every\nother code task finished before `configure` started. Do not build again. Hand\noff its command, result, and uploader summary lines as your own, and say you\nreused them. Otherwise:\n\n1. Install with the project's package manager, so the build sees the manifest\n and lockfile this run left behind. In a workspace package, that is the\n manager whose lockfile sits at the workspace root above this directory.\n2. Run the production build the project defines in this checkout: its package\n manager's build script, or the platform's compiler (Cargo, Go, Gradle,\n Xcode). You cannot run a Dockerfile or a CI job; when that is the only\n build, say so and finish. When the project defines no build, run its\n typecheck or lint script instead; when it defines none of them, say so and\n finish.\n3. Read the exit status first, then the log. When source-map or debug-symbol\n upload is wired and the credentials are written, the log shows the uploader\n creating a release and uploading files. A green build with no upload lines\n means the upload never ran: compare the variable names the build config\n reads with the names in the env file (`check_env_keys`). When the\n `credentials` handoff reports no key, the build still has to pass, with the\n upload skipped.\n4. Fix what this run's changes broke, in the files the handoffs name, and build\n again. A failure that was there before this run is not yours: say so in your\n handoff and leave it. After a few attempts, stop and hand off what still\n fails.\n\nBuild, never run. A command that installs or launches the app (`expo run:*`,\n`react-native run-*`, `flutter run`, `./gradlew install*`) opens a simulator\nor an emulator on the user's machine. On an Expo app whose `android/` and\n`ios/` folders are generated, build the JavaScript bundle instead: `npx expo\nexport --source-maps --platform `.\n\nWhen the upload is a separate `posthog-cli` command rather than part of the\nbuild, or the build sits in a `make` target or a shell script, you cannot run\nthose: run the build command itself, and say so. Never add a package script to\nget past a blocked command. Your shell gets a reduced\nenvironment: `ANDROID_HOME` and other variables from the user's shell do not\nreach it. A build that cannot find an SDK (`SDK location not found`) failed\nbecause of this shell, not the project: do not retry it, and do not give the\nuser a setup step for it. A missing toolchain, or a local interpreter too old\nfor a version this run pinned, is the environment, not the setup: say so and\nfinish, and never lower a version to make a local install pass.\n\nFinish this task `done` even when the build still fails. Your handoff gives\nthe command, the error, and whether this run caused it, and the report turns\nit into a follow-up. A `failed` verify stops the report, and the user gets no\nreport at all.\n\nA build with the credentials in place creates a release and uploads to the\nuser's project. That is expected: it is what every production build will do,\nand it puts the first symbol set in PostHog right away.\n\n## How you know you succeeded\n\nThe production build passes with this run's changes, and when the upload is\nwired with credentials, its log shows the release and the upload. Your handoff\ngives the command you ran, or that you reused `configure`'s build, its result,\nthe uploader's summary lines (release name and how many files it uploaded),\nevery fix you made with its file, and anything that still fails, so the report\ncan tell the user what was verified. The task ends `done` either way.\n", - 'wire-ci': - "---\ntype: wire-ci\nflow: error-tracking\nlabel: Set up CI for automatic uploads\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [configure, credentials]\n---\n\n## Goal\n\nMake the credentials reach the production build wherever it actually runs, so\nsource maps upload on every deploy — not just on a local build. Install the\nskill your task input names (`install_skill` with the `skillId`) and follow its\n**\"Set up CI for automatic uploads\"** step — it owns tracing where the\nproduction build runs and wiring the credentials through every layer, whatever\nthe CI provider.\n\nThe `configure` and `credentials` handoffs already name the build-config keys\nand the exact environment-variable names in use — carry those same names into\nthe pipeline; do not invent new ones. Trace the deploy path by reading the\nproject's own files (CI workflows, Dockerfiles, deploy scripts) — never invent\nconfig that is not there. You cannot create the CI secret that holds the API\nkey; reference it by name and carry that follow-up, plus any deploy path you\ncould not trace, into your handoff for the report.\n\nA package inside a workspace rarely holds its own pipeline. The CI config and\ndeploy files usually sit at the repository root, above this directory\n(`../.github/workflows`, `../../.gitlab-ci.yml`): look there before you\nconclude there is none, and wire the job that builds this package. When you\ncannot open them, name the file and the job in your handoff.\n\nThe user merges this change before they create the secrets. A step that fails\nwhen a PostHog variable is empty (`${VAR:?…}`, an `exit 1`, a required input)\nbreaks every deploy until they do. Pass the variables through and let the\nupload skip itself when they are empty: the build config's gate does that, and a\n`posthog-cli` step in the pipeline needs a condition on the key. Name the\nsecrets in your handoff instead of enforcing them.\n\n## Two things cross the boundary, not one\n\nCredentials are half of it. The uploader also needs a **release identity** — a\nname and a version — and it derives that from the CI's own git variables or\nfrom a `.git` directory. A container build sees neither: `.git` is almost\nalways in `.dockerignore`, and the CI's variables stop at the `docker build`\ncommand. Nor does a hardcoded release name rescue you; the uploader wants both\nhalves, and stops the build when it has only one.\n\nSo forward the provider's git variables into the build the same way you forward\ncredentials, and declare each one as `ARG` **and** `ENV` — `ARG` alone is not\nvisible to the uploader's environment lookup. Your skill's \"Associate the\nrelease with a git commit\" step lists the variables per provider. Read that\nstep even though it is not the CI step: this boundary is where it applies.\n\nForward them only where a provider actually sets them. A project with no\npipeline — an image built and run by hand — has nothing to inherit from, and\nvariables declared but never filled resolve to no release at all: it reads as\nwired and still fails the build. There the identity has to be supplied outright,\nboth halves, from something the build itself holds — a build argument the\noperator passes, or the manifest's own version — so name and version are always\npresent. Decide which case you are in by reading the repo, not by assuming a\nprovider exists.\n\nThat `ARG`-plus-`ENV` shape is for the git variables and the non-secret settings\nonly. The API key is a secret and keeps whatever secret-carrying mechanism the\nbuild system offers — a build secret mounted for the one step that needs it, a\nmasked variable, a secret file. A secret in `ARG` or `ENV` is recorded in the\nbuild history, and the builder itself will warn you: *do not use ARG or ENV\ninstructions for sensitive data*. Widening the git-variable pattern to cover the\nkey is a downgrade, not consistency.\n\nA build cache between the pipeline and the build, such as Turborepo's, is one\nmore boundary. List the upload credentials among the task's hashed inputs\n(Turborepo's `env`), so a build that has them never replays an entry made\nwithout them, and pass the git variables through without hashing them\n(`passThroughEnv`). Leave the cache on: a hit restores output whose chunks are\nalready uploaded.\n\n## The runtime boundary has names too\n\nUpload credentials are a build-time concern. The app also reads its own\nvariables at **run** time, and the step that starts it is another place a name\nhas to match — a `docker run -e`, a compose file, a systemd unit, a platform's\nenvironment settings. That step was written before PostHog existed here, so the\nname it passes is whatever the project used back then. If `init` settled on a\ndifferent one, the deployed process starts with an undefined key.\n\nNothing fails loudly. In production the init guard returns quietly instead of\nthrowing, so the process boots, serves traffic, and reports nothing — the same\nsilent shape as a mismatched upload variable, one boundary later.\n\nSo read the names the app's own source reads, and make the start step pass\nexactly those. Rename the deploy step's variable, not the code: the code is the\nhalf that already exists. Name any secret the user must create under its new\nname in your handoff.\n\n## How you know you succeeded\n\nThe pipeline that runs the production build carries the upload credentials by\nthe same names the credentials task used and reaches the uploader with a\nresolvable release identity, and the step that starts the app passes the\nvariable names the app's own source reads. Every secret the user still has to create is named\nin your handoff. Your handoff lists the CI files you changed\nand every manual follow-up, so the report can hand them to the user.\n", + 'capture-exceptions': captureExceptions, + configure, + credentials, + init, + install, + 'link-releases': linkReleases, + report, + 'setup-error-tracking': setupErrorTracking, + 'test-setup': testSetup, + verify, + 'wire-ci': wireCi, }; diff --git a/src/programs/feature-flags/agent-prompts.ts b/src/programs/feature-flags/agent-prompts.ts index b8366dce5..2b37fa242 100644 --- a/src/programs/feature-flags/agent-prompts.ts +++ b/src/programs/feature-flags/agent-prompts.ts @@ -1,16 +1,16 @@ -// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. +import createFlag from './agent/create-flag.md'; +import evaluate from './agent/evaluate.md'; +import init from './agent/init.md'; +import install from './agent/install.md'; +import report from './agent/report.md'; +import setupFeatureFlags from './agent/setup-feature-flags.md'; -/** This program's agent prompts by task type, verbatim. */ +/** This program's agent prompts by task type: each `agent/.md`, imported as text. */ export const AGENT_PROMPTS: Readonly> = { - 'create-flag': - "---\ntype: create-flag\nflow: feature-flags\nlabel: Create the example flags in PostHog\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-mcp]\nallowedTools: [Read, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure this PostHog project has one example flag for each side in your\n`sides` input, and none for a side that is not listed:\n\n| Side | Key | Name |\n|---|---|---|\n| `backend` | `wizard-example-backend-flag` | Wizard example backend flag |\n| `frontend` | `wizard-example-frontend-flag` | Wizard example frontend flag |\n\nFor each flag you need:\n\n1. Look it up by key with `feature-flag-get-definition-by-key`. One keyed\n lookup per flag; never list every flag in the project.\n2. Found: reuse it as it is. Do not change its rollout or its active state.\n3. Not found: create it with `create-feature-flag`, using the key and name\n above, a boolean flag with one release condition at 0% rollout, and\n `active: false`. New flags are active by default, so pass\n `active: false` explicitly. The user turns it on when they are ready.\n4. Create rejected because the key already exists: another run created it\n after your lookup. Look it up by key once more and reuse it as in step 2.\n\nRun `info` on each tool before calling it; never guess the input shape.\n\nIf a call is rejected for permissions, or the tool is hidden for a missing\nscope, stop and say so in your handoff: the credentials need\n`feature_flag:read` and `feature_flag:write`. Do not retry with other\ntools.\n\n## How you know you succeeded\n\nYour handoff gives, for each flag: its side, key, id, whether you created or\nreused it, whether it is active, and its URL in PostHog (the tool result's\nURL, verbatim).\n", - evaluate: - "---\ntype: evaluate\nflow: feature-flags\nlabel: Evaluate the flags in the app\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-feature-flags-step]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nFor each flag the create-flag handoff names, add one evaluation on that\nflag's side of this app: the backend flag in server code through the server\nSDK, the frontend flag in browser or device code through the client SDK.\nNever evaluate a flag on the other side. Your skill carries the SDK flag docs\nand the best-practices page; follow them for the exact calls.\n\nThe SDKs are installed and initialized, either already or by the tasks before\nyou. Build on that; do not re-check it.\n\nFor each flag, choose a call site where a real request or screen passes\nthrough. On the backend, evaluate once for that request and pass the value\ndown; never re-evaluate deeper in the call stack. Put the keys in a constants\nmodule the call site imports, one module per language; a template script that\ncannot import it gets the key from the server's constant, never a literal.\nEvaluate with the identified user's distinct id, the same id on both sides,\nand report each evaluation back to PostHog the way the docs show for that SDK.\nGate something harmless on each value, such as a log line or a small label,\nso the app behaves the same while the flags are off.\n\nThis is an edit-only task. Do not install dependencies, run the build, or\nstart the app.\n\n## How you know you succeeded\n\nEach flag is evaluated once, at one call site on its own side, with its key\nimported from a constants module, and its evaluation is reported. Your\nhandoff names, for each flag, the files changed and the call site, plus how\nthe distinct id is chosen on each side.\n", - init: "---\ntype: init\nflow: feature-flags\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep, check_env_keys, set_env_values]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure PostHog is initialized on each side in your `sides` input. An\nexisting init counts only when the env variable it reads is defined: check it\nwith `check_env_keys`. When a side's init is complete, leave it alone and\nsay so. Otherwise create or finish it following your skill, including the\nenv wiring and `.env.example`. A template-script frontend gets the PostHog\nsnippet in its base template.\n\nInitialize each SDK so a flag can be evaluated, and stop. Do not evaluate any\nflag; the evaluate task owns that.\n\nYou do not install packages, run builds, linters, or tests, or start the app.\nThe install task owns packages, and your edits just need to be right by\nreading.\n\n## How you know you succeeded\n\nPostHog initializes from a defined key on every side in `sides`. Your\nhandoff names, for each side, the init file, the client the evaluate task\nshould use, and the env variable names (never values).\n", - install: - "---\ntype: install\nflow: feature-flags\nlabel: Add the PostHog SDKs to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure each side in your `sides` input has its PostHog SDK in the\nmanifest: the server library for `backend`, the browser or mobile library\nfor `frontend`. Leave an SDK that is already there alone and say so.\nInstall a missing one following your skill, which owns the package manager\nand version rules.\n\nA frontend that is only scripts in server-rendered templates has no package\nto install: the init task adds the PostHog snippet there. Install nothing for\nthat side, and install nothing beyond these SDKs.\n\n## How you know you succeeded\n\nEach side's SDK is declared in the manifest at a real version, or your\nhandoff says plainly why the environment stopped you. Your handoff names the\nmanifest and package for each side.\n", - report: - "---\ntype: report\nflow: feature-flags\nlabel: Write the setup report\nsink: true\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-notebook, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nCompose the setup report from `read_handoffs` only, then publish it with a\nsingle `publish_handoff` call. The same call mirrors the markdown into a\nshareable PostHog notebook. The notebook skill says what the result means and\ncovers older wizards. Do not write a report file. The handoff call and the\nnotebook are how the report reaches the user.\n\nWrite one section per flag:\n\n- The flag: its side, key, URL, whether it was created or reused, and whether\n it is active, as the handoff gives it. Say a flag stays off until the user\n enables it only when the handoff marks it inactive.\n- Where the app evaluates it: files and call site, the constants module, and\n how the distinct id is chosen.\n\nThen:\n\n- If the run installed or initialized an SDK, say which and on which side.\n- How to confirm it works: open the page or endpoint that evaluates each flag,\n then find the evaluation on the flag's page in PostHog. It can take a few\n minutes to show up.\n\nThe run does not check that an evaluation reached PostHog, so never write that\none did. Where a step failed or was skipped, say so plainly instead of rounding\nit up to success.\n\n## How you know you succeeded\n\nOne `publish_handoff` call went through with the full report, and a user who\nreads only it knows which flags were created, which side evaluates each and\nwhere, which are still off and need turning on, and how to confirm an\nevaluation reaches PostHog. Every claim in it traces to a handoff. The notebook\nmirror is best effort: when it was created, the user has its link, and when it\nwasn't, your final message says why. A missing notebook never fails this task.\n", - 'setup-feature-flags': - '---\ntype: setup-feature-flags\nflow: feature-flags\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan example PostHog feature flags for this project and seed the task queue.\nThe end state: one example flag per side of the app that exists, each\nevaluated once in one place on its own side and reported back to PostHog, and\na report in a PostHog notebook telling the user how to turn the flags on.\n\nFirst establish two facts from the repo.\n\n**1. Which sides does the app have?**\n\n- `backend`: server code that handles requests, such as route handlers, API\n views, controllers, server components, or server loaders.\n- `frontend`: JavaScript or device code the app owns that runs in a browser\n or on a device, such as client components, a single-page app, inline\n scripts in its page templates, script files its pages load (directly or\n through asset helpers such as import maps or Vite), or mobile screens.\n\nA side counts only when the app already has code there to put an evaluation\nin. Never add a side to host a flag. Server-rendered templates that load no\nJavaScript of the app\'s own are not a frontend: such an app has only a\nbackend. An API has only a backend; a mobile app or a static single-page app\nhas only a frontend.\n\n**2. Is PostHog already integrated on each of those sides?** Look for the\nPostHog SDK for that side in the dependency manifests (or, for template\nscripts, the PostHog snippet) and an init call in the source. An init counts\nonly when the key it reads is defined in the repo\'s committed env template or\nbuild config. When you cannot tell, treat it as missing: the init task\nre-checks and leaves a complete init alone.\n\nThen seed the graph. Give every task the same input: `sides`, the list of\nsides you found (`["backend", "frontend"]`, `["backend"]`, or\n`["frontend"]`). Pass no other inputs; each task learns the rest from the\nhandoffs before it.\n\n- `install`, only when an SDK a side needs is missing from the manifest.\n- `init`, whenever an init is missing or unproven on either side.\n- `create-flag`, with no dependencies. It only talks to PostHog.\n- `evaluate`, after `create-flag` and after whichever of `install` and\n `init` you queued.\n- `report`, after every other task.\n\nNever plan capture, dashboard, or session-replay work. This run sets up the\nexample flags, nothing else.\n\n## How you know you succeeded\n\nEvery task above is queued with that dependency shape and the `sides` input,\n`report` depends on every other task, and your plan states which sides the\napp has, whether PostHog is integrated on each, and the files that told you.\nKeep labels short.\n', + 'create-flag': createFlag, + evaluate, + init, + install, + report, + 'setup-feature-flags': setupFeatureFlags, }; diff --git a/src/programs/metrics/agent-prompts.ts b/src/programs/metrics/agent-prompts.ts index d5103fc9b..b73b4be4f 100644 --- a/src/programs/metrics/agent-prompts.ts +++ b/src/programs/metrics/agent-prompts.ts @@ -1,13 +1,12 @@ -// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. +import instrumentMetrics from './agent/instrument-metrics.md'; +import report from './agent/report.md'; +import setupMetrics from './agent/setup-metrics.md'; +import verifySdk from './agent/verify-sdk.md'; -/** This program's agent prompts by task type, verbatim. */ +/** This program's agent prompts by task type: each `agent/.md`, imported as text. */ export const AGENT_PROMPTS: Readonly> = { - 'instrument-metrics': - '---\ntype: instrument-metrics\nflow: metrics\nlabel: Instrument the service with metrics\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [verify-sdk]\n---\n\n## Goal\n\nInstrument your service with application metrics. When your task input names\na `package`, stay inside it — sibling packages belong to other chains.\nInstall the platform\'s skill the way the verify handoff names it (`load_skill_menu` category\n"metrics", then `install_skill`), and follow it end-to-end — it owns where\nmetrics belong (request middleware, background\njobs, external calls, business commit sites), which of counter, gauge, and\nhistogram fits each site, and the low-cardinality attribute rules.\n\nImport the client the way the verify handoff names it. Only additive changes:\nadd metric calls, never restructure the code they land in.\n\n## How you know you succeeded\n\nThe hot paths and business commit sites record metrics through the one shared\nclient, the project still builds, and your handoff lists every metric name\nwith its type and where it fires.\n', - report: - "---\ntype: report\nflow: metrics\nlabel: Report and hand off\nsink: true\nmodel_pi: openai/gpt-5.6-luna\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-notebook]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: [instrument-metrics]\n---\n\n## Goal\n\nTell the user what their service now measures, from the handoffs of every task\nin the run. Briefly and concretely:\n\n- Each metric now recording: its name, type (counter, gauge, histogram), and\n where in the code it fires.\n- The service name the client reports under, and the SDK version the run\n verified or installed.\n- Anything skipped or deferred, with the reason — an environment that blocked\n an upgrade is an outcome, not an apology.\n- Where results appear: the Metrics page in PostHog, with the first series\n arriving as traffic flows.\n\nIf the run also installed or initialized the SDK, say so — the user started\nthis command without PostHog and now has it.\n\nPublish the report with a single `publish_handoff` call. The same call mirrors\nthe markdown into a shareable PostHog notebook. The notebook skill says what the result means and covers\nolder wizards. Do not write a report file. The handoff call and the notebook\nare how the report reaches the user.\n\n## How you know you succeeded\n\nA user who reads only your report knows what their service measures, where\neach number comes from, what still needs them, and where to look first. The\nreport went through one `publish_handoff` call. The notebook mirror is best\neffort: when it was created, the user has its link, and when it wasn't, your\nfinal message says why, and when an update failed, that the notebook holds the earlier version. A missing notebook never fails this task.\n", - 'setup-metrics': - '---\ntype: setup-metrics\nflow: metrics\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan an application-metrics setup and seed the task queue. Metrics are a\nper-service concern: every service gets its own SDK install and its own\n`service_name`.\n\nFirst map the repo — mechanically, before any judgment. Run one `find` per\nmanifest pattern across the whole tree, every time, even when the root looks\nlike a plain npm workspace (repos mix ecosystems, and the workspace list in a\nroot manifest is not the whole map):\n\n **/package.json **/pyproject.toml **/requirements.txt **/Pipfile\n **/go.mod **/composer.json **/Gemfile\n\nEvery directory holding a hit is a package. Classify each: a **service** runs\nserver-side work (server entrypoints, workers, APIs — whatever the language);\nbrowser-only apps and libraries are not services and get no chain (the report\nnotes them). Ignore installed-dependency and scaffolding directories\n(`node_modules`, `.claude`, caches) — they are not packages.\n\nThen seed, per service:\n\n- `verify-sdk`, with no dependencies — it settles that service\'s SDK question\n (installed, new enough, initialized for metrics) before any\n instrumentation. Pass `inputs: { package: "", service_name: "" }`\n so the task knows which package it owns; omit `package` in a single-package\n repo.\n- `instrument-metrics`, after that service\'s `verify-sdk`, with the same\n inputs.\n\nChains for different services are independent — do not wire edges between\nthem, so they run in parallel. Last, one `report` that depends on every\n`instrument-metrics`. It writes the handoff last, so it describes what\nactually shipped.\n\nMetrics ride each service\'s existing PostHog SDK (`posthog.metrics`), so the\ngraph is the same whether or not the repo has PostHog today — `verify-sdk`\nabsorbs the difference. Never plan an identify, capture, or dashboard task:\nthis run sets up application metrics, not the full integration.\n\n## How you know you succeeded\n\nEvery service has its own verify → instrument chain carrying its package and\nservice name, the chains share no edges, the report depends on all of them,\nand every `verify-sdk` is runnable. Keep labels short — the action in a few\nwords, naming the service when there are several.\n', - 'verify-sdk': - "---\ntype: verify-sdk\nflow: metrics\nlabel: Verify the PostHog SDK is ready for metrics\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Edit, Glob, Grep, Bash, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nLeave your service with a PostHog SDK that can record metrics. When your\ntask input names a `package`, that package is your whole world — its\nmanifest, its init site, its `service_name`; do not touch sibling packages.\nFirst call\n`load_skill_menu` with `category: \"metrics\"` and `install_skill` for the\nvariant matching the project's platform — the menu is the source of truth,\none variant per platform, and a full-stack app takes the server variant\nbecause metrics measure service work. The installed skill carries the\nplatform's install and init specifics. Then three checks, in order:\n\n1. **Installed?** Look for the SDK in the dependency manifest. Missing →\n install it with the project's own package manager and let it resolve the\n version — never invent one.\n2. **New enough?** `posthog.metrics` ships in recent SDK releases only — the\n skill states the version floor for this platform. Below it → bump with the\n package manager's upgrade command. At or above → leave it alone and say so\n in your handoff.\n3. **Initialized for metrics?** Find where the client is constructed. Add the\n `metrics` config (a service name) to that existing client, as the skill\n shows. No client anywhere → initialize one, following the skill. Never\n construct a second client beside a working one, and never touch identify\n calls, event capture, or other init options.\n\n## How you know you succeeded\n\nThe manifest holds the SDK at a metrics-capable version and exactly one client\ninit carries the metrics config — or your handoff plainly says why the\nenvironment stopped you. Name the skill variant you installed, the manifest, the version, and the\ninit site in your handoff, so instrumentation installs the same skill and\nknows what to import and where.\n", + 'instrument-metrics': instrumentMetrics, + report, + 'setup-metrics': setupMetrics, + 'verify-sdk': verifySdk, }; diff --git a/src/programs/posthog-integration/agent-prompts.ts b/src/programs/posthog-integration/agent-prompts.ts index 97d96fcb2..811c125a6 100644 --- a/src/programs/posthog-integration/agent-prompts.ts +++ b/src/programs/posthog-integration/agent-prompts.ts @@ -1,27 +1,28 @@ -// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. +import aiObservability from './agent/ai-observability.md'; +import capture from './agent/capture.md'; +import dashboard from './agent/dashboard.md'; +import errorTracking from './agent/error-tracking.md'; +import identify from './agent/identify.md'; +import init from './agent/init.md'; +import install from './agent/install.md'; +import integratePosthog from './agent/integrate-posthog.md'; +import logs from './agent/logs.md'; +import report from './agent/report.md'; +import review from './agent/review.md'; +import warehouse from './agent/warehouse.md'; -/** This program's agent prompts by task type, verbatim. */ +/** This program's agent prompts by task type: each `agent/.md`, imported as text. */ export const AGENT_PROMPTS: Readonly> = { - 'ai-observability': - "---\ntype: ai-observability\nflow: integration-v2\nlabel: Add AI Observability\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: high\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [capture]\noptional: true\n---\n\n## Goal\n\nAdd AI Observability to the LLM calls this project already makes. Read the\nupstream handoffs and inspect the call sites, not just the dependency names.\nIf there are no LLM calls, complete as `not needed` with the reason and make no\nchanges. Do not install a vendor SDK, add a demo call, or use manual capture to\ninvent an AI feature in an app that has none.\n\nFor existing LLM calls, load the `ai-observability` skill menu and install the\nvariant matching the calling code's language and provider or agent framework.\nFollow that skill's selection rules and instrumentation references. Manual\ncapture is appropriate only for existing calls without a supported wrapper.\nInstrument against the provider SDK version the app already uses; never\nrequire, assume, or request an upgrade of the app's own dependencies — if the\nwrapper cannot support the app's version, fall back to manual capture or\nreport why. Reuse the PostHog client and identity established earlier in this\nrun. Follow the skill's privacy-mode instructions: default new SDK clients to\n`privacy_mode=False` (Python) or `privacyMode: false` (Node), so prompt and\ncompletion content is captured. Preserve existing privacy settings, redaction,\nand explicit user or project requirements. Use the variant's documented controls\nwhere capture is configured elsewhere; do not strip manual-capture content by\ndefault. If a variant cannot be chosen from the code, report the ambiguity as\n`not needed`; this default run does not ask the user to choose a provider.\n\nThis is the instrumentation part of a larger integration. Name the packages\nthis task needs in your handoff for review to install; do not edit dependency\nmanifests, and never write a version number you invented — the package manager\nresolves real versions when review installs by name. Defer the skill's\npackage-manager and import-check commands to that task. For Go, review adds the\nmodules with `go get` and `go mod tidy`; do not hand-edit `go.mod` or `go.sum`.\nDo not run the app or make paid LLM calls. Use `check_env_keys` and\n`set_env_values` for environment files, using the project credentials supplied\nby the wizard.\n\n## How you know you succeeded\n\nExisting LLM calls are wired using the selected skill, or the handoff clearly\nexplains why this task was not needed. List the variant, changed files, declared\ndependencies, and a concrete call path the user can trigger to verify the\nsession/trace/generation tree. Include the skill's **Privacy mode** handoff:\nthe effective setting, the file and line to edit, when to enable it, and the\n[privacy-mode docs](https://posthog.com/docs/ai-observability/privacy-mode). Pass any\ndeferred dependency work to review.\nDescribe delivery as unverified unless this run actually observed it; do not\npublish a separate setup report.\n", - capture: - "---\ntype: capture\nflow: integration-v2\nlabel: Capture events\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-capture, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init, identify]\n---\n\n## Goal\n\nDecide which events are worth capturing in this app, then instrument them in the\nsame pass — read each file once, choose the events, and add the capture calls\nwhile the file is already open. Instrumenting the events is the job; everything\nbelow is a rule you apply while you do it, not work of its own.\n\nThe identify step's handoff already says how this app attributes events. Read it\nonce, up front, and follow it:\n\n1. Where identity is established for you — a client library that remembers it, or a\n framework middleware that binds it per request — a plain capture is already\n attributed. Write the plain call and move on. Do not add a context of your own,\n and do not go looking through the call tree to prove it.\n2. Where the handoff says it is not — no middleware, or a call path that runs\n outside the request — tag that call with the distinct id and the session id.\n\nWhere the project has no identity to begin with — the identify step found nothing to\nwire, because the app has no accounts or login — personless events are the right\nanswer. Capture plainly, with no id and no placeholder, and say so in your handoff.\n\nThe placeholder is for the third case only: identity exists in this project, but no\nstable id reaches the call site. Do not guess at the id and do not invent a fallback —\npass `DISTINCT_ID` there so the gap is visible in the code, and say in your handoff\nthat identity exists but could not be reached, so the report calls it out as an issue\nto follow up rather than claiming events that belong to nobody.\n\nNever put PII in an event. When new information about the user surfaces, tag the\nuser the way the identify docs describe, not the event.\n\n## How you know you succeeded\n\nThe meaningful user actions across the app have capture calls that fire on the\nreal action, not on page load, each one attributable to the user who took it, and\n`.posthog-wizard-cache/.posthog-events.json` lists the events you instrumented.\n\nYou do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.\n", - dashboard: - '---\ntype: dashboard\nflow: integration-v2\nlabel: Create a starter dashboard\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-dashboard, integration-v2-insight, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [capture]\n---\n\n## Goal\n\nCreate a starter PostHog dashboard with a few insights built on the events this\nintegration instruments, using the PostHog MCP.\n\n## How you know you succeeded\n\nA dashboard exists with a handful of insights on the captured events, and you hand\noff its URL for the report to link.\n', - 'error-tracking': - "---\ntype: error-tracking\nflow: integration-v2\nlabel: Add error tracking\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-error-tracking-step, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nMake the errors the app does not catch reach PostHog, by whatever means the SDK\noffers for that. Which means depends on the SDK: some autocapture exceptions once\nyou enable it at init, some wire into the framework's own error handler, some give\nyou a boundary to mount at the app entry. Follow the docs and the reference example\nfor this one, and set it up in one place — never manual capture calls sprinkled\nacross files.\n\nThe SDK is already installed and initialized (see the context from previous steps);\nbuild on that, do not re-check it.\n\nThis is an instrument-only task. Do not install dependencies, run the build, run\ntests, or start the app — a later `build` step does all verification. Stay inside\nthis project's directory and set up that one place; that is the whole job.\n\n## How you know you succeeded\n\nAn error the app does not catch reaches PostHog, through the mechanism this SDK\ngives you rather than one you invented. You did not install anything, run a build,\nlint, or tests (the review task verifies after you), search outside the project, or read through the whole app or hand-wrap\nindividual components or routes.\n", - identify: - "---\ntype: identify\nflow: integration-v2\nlabel: Wire user identification\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-identify, posthog-best-practices]\nallowedTools: [Read, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nWire user identification so everything the app reports — its errors as much as its\nevents — carries who the user is.\n\nThe distinct id is the app's own stable identifier for the user — a primary key, a\nuuid, a resource name. Not an email and not a username: those are PII and they change;\nsend them as person properties instead. Fall back to email only when the app truly has\nno other stable key, and say so in your handoff — do not invent one by altering the\napp's schema.\n\nWork out first what you are in: a client-only app, a fullstack or SSR app, or a\nbackend or API. Identification takes a different shape in each, and the shape\nfollows from that answer:\n\n1. Client and frontend — identify once, and only once, when the user logs in or\n registers, and reset when they log out. That single call merges what was already\n captured against the anonymous id into the stable distinct id you give it. The\n library persists identity from there, so everything after it is attributed to the\n same user until reset — nothing needs re-identifying per page or per event.\n2. Fullstack or SSR — your identification hinges on the client side, where the\n identity is persisted. The distinct id and the session id travel on every request\n the frontend sends, in the header format the docs describe. Some libraries already\n do this for you; read the docs and the example app before wiring it by hand. The\n backend should take note of the session and distinct id as soon as possible and\n either carry it to every capture or use the identified context to identify the\n request.\n3. Backend and API — a server serves many users at once, so identity belongs to the\n request, never to the process. Take it from the incoming request or the ambient\n context, as far as the app allows. Then either tag each event with the distinct\n id and session id, or, where the SDK has contexts, open one around the whole\n request and identify that context so everything the request reports inherits it.\n Where the SDK ships a middleware for this framework, that is what does this — use\n it rather than writing your own. Read the docs and the example app.\n\n## How you know you succeeded\n\nIdentification matches the kind of app you are in. While a user is known, what the\napp reports is attributable to them with a stable distinct id — the errors it\nreports, not only the events — established at the boundary the SDK gives you rather\nthan repeated at call sites that already inherit it. Where the app changes who the\nuser is mid-request, that boundary cannot know it yet, and establishing identity\nagain there is the point, not a duplicate. Your handoff names the files you\nchanged, how identity is established in them, and what a later step must do for its\nown calls to inherit it — whether that is nothing at all, or tagging each call\nitself. If the app has no auth or user concept, say so and stop.\n\nYou do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.\n", - init: "---\ntype: init\nflow: integration-v2\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nInitialize PostHog: create the framework's init point so the SDK is configured\nonce and available across the app, set the PostHog environment variables through\nthe wizard tools, and document those keys in `.env.example` for other developers.\n\nTry to follow these principles, in order: codebase convention, don't make any\nunnecessary changes, keep the change as lean as possible for an easy review, and\nfollow the shape of the examples provided. How call sites reach the client is\npart of the shape: reach it the way the docs and the example project reach it.\n\n## How you know you succeeded\n\nThe init file exists and the PostHog env keys are present. Keys live in the env\nfile, never hardcoded in source, and `.env.example` lists the key names (with\nplaceholder values) so the next developer knows what to set. Your handoff names the\nfiles you changed, how the client is constructed in them, and how a call site\nreaches that client — every step after you has to reach the same one the same way.\nIf the app ships a Content-Security-Policy, the handoff also names exactly\nwhich directives you touched (`script-src`, `connect-src`, `worker-src`) and\nwhich you left alone — the review verifies directive-by-directive, and an\nunnamed directive reads as unhandled, not as fine.\n\nYou do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.\n", - install: - "---\ntype: install\nflow: integration-v2\nlabel: Add the PostHog SDK to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-haiku-4-5\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nInstall the PostHog SDK with the project's own package manager, so the real\npublished version lands in the manifest and the lockfile — not one you made up.\nRun the manager's add command with the bare package name and let it resolve the\nversion: `npm install ` (or the project's pnpm/yarn/bun), `pip install `,\n`composer require `, `bundle add `, `gem install `. Never invent a\nversion — one that was never published fails the whole run with `ETARGET`, and\nguessing another just fails again. The only version you write by hand is one copied\nfrom the framework reference example, and only in the environment fallback below.\n\nAdd the client library, plus the server library if the app has server-side code\nthat sends events. If the SDK is already installed, leave it and say so.\n\nSome ecosystems pin an explicit version in a manifest no install command resolves\n(Swift SPM, Gradle). There, declare the dependency the way the framework reference\nshows and take the version from the SDK's latest release — do not invent one.\n\nAttempt the install once. If it fails on something you own — a wrong package name or\nthe wrong manager — fix that and run it one more time.\n\nBut if the environment prevents the install through no fault of your change, do NOT\nspiral retrying it. These are environment failures, not yours:\n\n- a peer-dependency conflict the project already had (npm `ERESOLVE`),\n- a dependency already in the manifest that itself does not resolve (a bad or yanked\n version, a package that no longer exists) — the failure names a package that is not\n PostHog,\n- the package manager crashing on the project's existing dependency graph or a\n corrupt/incompatible lockfile (e.g. npm `Cannot read properties of null`),\n- the package manager itself failing to run or provision — corepack unable to\n switch to a pinned version (`Failed to switch pnpm to …`, `Cannot find matching\n keyid`), a missing manager binary, a toolchain the machine does not have,\n- no network or registry reachable.\n\nThe test: if the failure names your PostHog package or your command, it is yours to\nfix; if it names another dependency, a version pin the project set, or the toolchain\nitself, it is the environment — fall back and move on.\n\nWhen you hit one of those, fall back: add the PostHog package to the manifest by hand\nat a known-good version, so the dependency is still declared for the later steps and\nfor the user's own install. Use the framework reference example's spec if it names\none; otherwise use a caret on the SDK's current known-good release — `posthog-js`\n`^1.407.2`, `posthog-node` `^5.46.1`, `posthog-php` `^4.12.1`, `posthog` (Python)\n`^7.29.0`, `posthog-ruby` `^3.21.0`. The caret keeps it current without pinning a\npatch; never invent a version that was never published. Then move on, and say in your\nhandoff exactly what failed and the command\nyou ran, so the review step and the report surface it.\n\n## How you know you succeeded\n\nThe SDK resolved and installed at its real version, or you have reported plainly in\nyour handoff why the environment stopped it. Your handoff names the manifest and the\npackage, so later steps import it under the name they will actually get.\n", - 'integrate-posthog': - "---\ntype: integrate-posthog\nflow: integration-v2\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan a PostHog integration and seed the task queue with this graph:\n\n- `install` and `init`, independent of each other.\n- `identify` and `error-tracking`, each after `install` and `init` and independent\n of one another, so they run in parallel. `error-tracking` makes the errors the app\n does not catch reach PostHog, by whatever means the SDK offers for that — it needs\n the SDK installed and initialized, not the events.\n- `capture`, after `identify` — it decides the events and instruments them, and it\n reads how identity is already established before it instruments anything.\n- `ai-observability`, after `capture`, parallel to `error-tracking` — it instruments\n existing LLM calls, or reports that none apply. Queue it on every default run; the\n task checks applicability rather than the planner guessing from a package name.\n- `logs`, after `ai-observability` — it configures log capture on supported\n platforms, or reports why it does not apply. Queue it even when AIO will be\n skipped. These tasks run sequentially because both instrument the same entry\n and initialization files, and Logs preserves any tracing provider AIO\n configures — an ordering-dependent instruction. (Their dependency installs\n are deferred to review, so manifests are not the conflict.)\n- `review`, after `install`, `init`, `identify`, `capture`, `error-tracking`,\n `ai-observability`, and `logs` —\n it installs the dependencies, verifies the project builds/typechecks/lints, and\n reviews every change the run made, fixing what fails. There is no separate build\n step: verifying and reviewing are one pass over the same changeset.\n- `dashboard`, after `capture`, independent of AIO, Logs, and `review` — it builds insights from the\n instrumented events, which `capture` has already defined; it needs no code review.\n- `report`, after `dashboard` **and** `review` — it writes the setup report last, so\n it describes the integration as reviewed rather than as first written.\n\nIf `enqueue_task` does not offer a task's type, this run excludes that product\n— skip it rather than retrying, and hang nothing off it.\n\n## How you know you succeeded\n\nEvery task in the graph is queued with that dependency shape, the report last,\nand the first task runnable. Keep labels short — the action in a few words.\n", - logs: "---\ntype: logs\nflow: integration-v2\nlabel: Add log capture\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill]\ndisallowedTools: [enqueue_task]\ndependsOn: [ai-observability]\noptional: true\n---\n\n## Goal\n\nConfigure PostHog log capture for this project's supported runtimes. Load the\n`logs` skill menu, install its matching skill, and read the platform reference\nbefore editing. Inspect the runtime and existing logging setup; use the\ndocumented OTLP exporter or SDK-native log capture for that platform. If the\napp already uses OpenTelemetry, write against its existing OTel version and\nname exporter packages compatible with it — never require, or write code that\nassumes, an upgrade of a package this integration did not introduce. A\nbrowser-only app must not receive a server exporter. If no documented setup\napplies, complete as `not needed` and explain why.\n\nExport only log lines this integration adds. Create a dedicated logger for the\nPostHog exporter and emit a few purpose-written lines at meaningful points the\napp already passes through. Even where the platform reference attaches the\nexporter to the application's root or existing loggers, do not — the app's\nexisting logs are data this run has no mandate to export, and every line that\nleaves must be visible in this run's diff. Reuse any existing PostHog log\nexport and leave all existing handlers and outputs intact; if capture is\nalready configured, report that without adding a second exporter. Preserve any\ntracing provider AIO configured in the previous step. Do not export secrets,\nrequest bodies, or user data, and do not scatter logs through unrelated code.\n\nThis is the instrumentation part of a larger integration. Instead of the\nstandalone skill's install command, name the required packages in your handoff\nand leave installation, manifest and lockfile updates, builds, and lint to\nreview. Do not edit dependency manifests or write a version number you\ninvented — the package manager resolves real versions when review installs by\nname. Use `detect_package_manager` if needed. Use\nthe wizard's supplied project and region and the env names from upstream\nhandoffs; inspect and write environment files only through `check_env_keys`\nand `set_env_values`. Do not request credentials or select a different project.\nIf required configuration is unavailable, report what is missing instead of\nguessing a region.\n\n## How you know you succeeded\n\nLog capture is configured once for the supported runtime, was already present,\nor has a clear skip reason. The handoff names the changed files, packages, env\nvariable names (never values), and any dependency work for review. Give a\nspecific code path the user can trigger to produce one of the added log lines\nand find it in PostHog Logs; delivery remains unverified until observed. Note\nfor the report that only lines added by this run are exported, and the one-line\nchange that routes the app's existing loggers into the same exporter if the\nuser wants more. Do not publish a separate report.\n", - report: - "---\ntype: report\nflow: integration-v2\nlabel: Write the setup report\nsink: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [integration-v2-report, integration-v2-notebook, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: [dashboard, review]\n---\n\n## Goal\n\nCompose the setup report summarizing what this integration did, drawing only on\nwhat the run itself recorded: the queue log and event plan in\n`.posthog-wizard-cache/` (`queue.json` and `.posthog-events.json`), and the\nhandoff each step left behind. Then publish it with a single `publish_handoff`\ncall. The same call mirrors the markdown into a shareable PostHog notebook. The notebook skill says what the result\nmeans and covers older wizards. Do not write a report file. The handoff call\nand the notebook are how the report reaches the user.\n\nSeparate what the run verified from what it did not. A passing build proves the\ncode compiles, not that events flow — never write that an event was captured\nunless the run observed it arrive. Where a step failed or was skipped, say so\nplainly instead of rounding it up to success. Anything only the user can confirm\nbelongs in a checklist to work through before merging, each item naming the file\nand line to look at.\n\nWhere a handoff carries a report section, that step already wrote the part of\nthe report it owns. Include it as its own section instead of rewriting or\nsummarizing it, and do not restate its contents elsewhere.\n\nWhere a handoff reports something a step could not resolve — attribution it could not\nestablish, a question the step before it left open — that is not a caveat to bury in\nprose. Raise it as its own issue to follow up, saying what is unresolved and what it\ncosts if left alone. A `DISTINCT_ID` placeholder left at a call site means no stable id\nwas available: name every file and line carrying one, so the user knows what to replace\nbefore those events mean anything.\n\n## How you know you succeeded\n\nOne `publish_handoff` call went through with the full report: what was\ninstalled and initialized, the events captured, whether identify was wired or\nskipped, error tracking added, AI Observability and Logs configured, skipped, or\nfailed with reasons, the dashboard link, any build conflict in full,\nand the next steps for the user. Every claim in it traces to a handoff, and\nwhat the run could not confirm reads as unconfirmed. The notebook mirror is\nbest effort: when it was created, the user has its link, and when it wasn't,\nyour final message says why, and when an update failed, that the notebook holds the earlier version. A missing notebook never fails this task.\n", - review: - "---\ntype: review\nflow: integration-v2\nlabel: Verify and review the integration\nmodel_pi: openai/gpt-5.6-sol\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: [posthog-best-practices, integration-v2-build]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: [install, init, identify, error-tracking, capture, ai-observability, logs]\n---\n\n## Goal\n\nYou are the last hands on the code. First make the integration run, then review it,\nthen leave it building to the best of your ability. Do both in one pass, verifying and reviewing are the same\nreading of the same changeset.\n\n**Install and verify.** The earlier steps edited code but did not install —\ninstall now, then verify the project builds, typechecks, and lints, whichever\nof those it defines. Install what the manifest declares plus every package the\nupstream handoffs name, adding the named ones by bare name with the project's\npackage manager so it resolves real versions. The app's existing dependencies\nare not yours to touch: never upgrade, downgrade, or re-add a package this\nintegration did not introduce. If an instrumentation package's peer range\nconflicts with a dependency the app already has, install an older\ninstrumentation version that fits the app's existing versions — find it with\nthe package manager, stepping down major versions (`pkg@^7`, then `@^6`,\n…) for a few attempts rather than looking for documentation of one. If none\nfits, revert that piece of instrumentation, leave the app's dependency alone,\nand record the incompatibility so the report can say what was skipped and why. An optional upstream task\n(AI Observability, Logs) may have failed and left partial edits no handoff\ndescribes — install and build either way, and let the build surface stray\nimports; fix or revert them like any other defect. Do not run the test suite; the\nruntime does not allow it, and a green build is the bar. A bad version only surfaces\nhere: if the install fails because a declared version does not exist (npm `ETARGET`,\n\"no matching version\", a yanked release), do not guess pins — reinstall the\npackage by bare name and let the package manager pick, matching the framework\nreference example's spec only if that also fails. If the failure is the environment, not the\nintegration's own change (a pre-existing broken dependency, a package manager that\ncannot provision), note it and move on; do not spiral.\n\n**Review, as a strict reviewer who did none of the writing.** The changeset is\nalready handed to you: `read_handoffs` returns every upstream task's handoff, and\neach one names the files it touched and what it changed in them. That union of\ntouched files IS the changeset. Read each changeset file once and judge it — after\nyour own edit you already know its contents, so do not re-read it. Read beyond the\nchangeset only where a dimension sends you: one neighboring example per pattern you\nare checking, and the callers of code that changed. Nothing else — not the SDK's\ninstalled internals, not other tasks' instructions, not a diff of the whole tree;\nthe handoffs are the signal.\nThe handoffs also carry what no file shows — env values written through\n`set_env_values`, manifest edits. Judge each change against these dimensions, in\norder:\n\n1. Minimal — every line earns its place. Delete helpers nothing calls, defensive\n wrapping around code that cannot fail, config for features that were not set up,\n and comments that narrate what a line does or that it was added.\n2. Unrelated — the run must not have changed behavior outside the integration.\n Revert reformatting, renames, or edits in code the integration never needed.\n3. Codebase patterns — for each kind of edit (client construction, how call sites\n reach it, config and env access, imports, naming, comment density), find one\n neighboring example in this project and compare. If a change invented a seam the\n project does not have — a registry, locator, wrapper, or app the codebase never\n uses — replace it with the closest pattern the project actually uses.\n4. Example shapes — where the framework reference example shows a shape for init or\n instrumentation, the code should be recognizably that shape adapted to this\n codebase. Codebase idiom beats the example when they conflict; the PostHog\n correctness rules you were given beat both.\n\nYou never change what is captured — event names, properties, and where events fire\nare a contract the dashboard and report are built on. Refactor how, and kill bad\nchanges; never alter the what.\n\nEvents reaching PostHog in production is the point of this code, so a capture wired\nto a place it will never fire, an uninitialized SDK, or a call the runtime silently\ndrops is squarely yours to catch and fix — it is a code defect. What you cannot do is\nconfirm delivery: you cannot exercise the app, and you never treat \"I could not\nverify events arrive\" as a finding.\n\nStay inside verify-and-review. These are not your job:\n\n- The report or the dashboard — other tasks own those; do not write or judge them.\n- Product or analytics design — which events are worth tracking, identity strategy,\n framework-version tradeoffs. The changeset is the decision; review whether the code\n implements it correctly, not whether it was the right decision.\n- Anything the integration did not touch. Do not widen scope to pre-existing issues.\n\nIf you notice something real but out of scope, leave it — a one-line mention in your\nhandoff, no more. Only flag what you can pin to a specific line and dimension — no\ntaste-based rewrites, and never expand the integration. For each fix, ask what would\nbehave differently if it were wrong, and check by reading the callers. After any\nchange, re-run the build or lint and confirm it still passes.\n\n## How you know you succeeded\n\nThe project installed and builds, typechecks, and lints as well as it did before the\nrun; you can name each review dimension and say it passes; and your fixes are in the\nfiles. If the build or lint fails only on pre-existing errors you did not introduce,\nthat still counts — put a one-line summary in the `conflict` field and the detail in\nwhat you did. Reserve a failed status for when your own changes break the build. Put\nthe number of fixes and the dimension each addressed in your handoff.\n", - warehouse: - "---\ntype: warehouse\nflow: integration-v2\nlabel: Connect your data sources\nrunnerSeeded: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-warehouse, integration-v2-mcp]\nallowedTools: [Read, Glob, Grep, wizard_ask, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nConnect the data sources listed in your task input to PostHog's data warehouse,\nso the project's own data can be queried next to the events this run\ninstruments. The wizard found them in the project; your job is to turn each one\ninto a real connection, or into a link the user can finish in the browser.\n\nYou are the only step that stops to ask the user for anything, so make the ask\ncount: send the user to that source's secure connect page in one `wizard_ask`\ncall, tagged with that source's kind as its `subject`. The user enters the\ncredentials on that page, so no secret passes through you. A decline answers\nthat one source. Move on to the next source rather than re-asking or ending the\nround.\n\nTouch no project code. This step connects data; it does not edit the app.\n\n## How you know you succeeded\n\nEvery source in your input reached a decided end: connected in PostHog, handed\nback as a link to finish, or skipped with a reason. Your handoff carries a\nfinished report section naming each source and which of those three it was, so\nthe reporting step can include it as it stands.\n\nA link is a handoff, not a connection. Your report section opens with the count\nyou connected, and your `complete_task` status matches it: `done` only when you\nconnected at least one source.\n", + 'ai-observability': aiObservability, + capture, + dashboard, + 'error-tracking': errorTracking, + identify, + init, + install, + 'integrate-posthog': integratePosthog, + logs, + report, + review, + warehouse, }; diff --git a/src/programs/replay-vision/agent-prompts.ts b/src/programs/replay-vision/agent-prompts.ts index 72cd3bb87..44627067f 100644 --- a/src/programs/replay-vision/agent-prompts.ts +++ b/src/programs/replay-vision/agent-prompts.ts @@ -1,20 +1,20 @@ -// Generated by `pnpm bundle:agent-prompts` from ./agent/*.md. Do not edit. +import enableReplay from './agent/enable-replay.md'; +import init from './agent/init.md'; +import install from './agent/install.md'; +import report from './agent/report.md'; +import scannerBrokenExperiences from './agent/scanner-broken-experiences.md'; +import scannerSessionSummaries from './agent/scanner-session-summaries.md'; +import scannerUserFrustration from './agent/scanner-user-frustration.md'; +import setupReplayVision from './agent/setup-replay-vision.md'; -/** This program's agent prompts by task type, verbatim. */ +/** This program's agent prompts by task type: each `agent/.md`, imported as text. */ export const AGENT_PROMPTS: Readonly> = { - 'enable-replay': - '---\ntype: enable-replay\nflow: replay-vision\nlabel: Make session replay record\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-enable-replay]\nallowedTools: [Read, Edit, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Bash, enqueue_task]\ndependsOn: [install, init]\n---\n\n## Goal\n\nMake session replay actually record. Recording has a server half and a client\nhalf, and either one can silently cancel the other.\n\nServer half: call `products-enable` (via `exec`: `info products-enable`, then\n`call products-enable {"products": ["session_replay"]}`). It is idempotent and\nserver-owned; `"enabled"` and `"already_enabled"` are both success.\n\n- **Tool missing on this deploy**: don\'t fail the task — record a follow-up in\n your handoff telling the user to turn on "Record user sessions" under\n Settings → Session replay, and finish.\n- **Permission rejection**: record a follow-up to enable it from a\n project-admin account, and finish — scanners created later sit idle until\n recording is on, then start working with no re-setup.\n\nClient half, web apps only: check the `posthog.init(...)` options.\n`disable_session_recording: true` cancels the server flip — remove it (or set\nit `false`). If nothing overrides recording, leave the init alone. This is the\nonly code edit this task may make: never restructure the init, never add new\ninstrumentation. If you can\'t confidently locate or edit the init, record a\nfollow-up instead of guessing.\n\nPure backend or mobile app with no web surface: nothing records browser\nsessions here. Say so in your handoff and finish — scanner tasks read your\nhandoff and skip what doesn\'t apply.\n\n## How you know you succeeded\n\nRecording is on server-side and nothing in the client init cancels it — or\nyour handoff names exactly which half needs the user and why. Later tasks\ntrust your handoff, so state plainly whether recordings will flow.\n', - init: "---\ntype: init\nflow: replay-vision\nlabel: Set up PostHog initialization\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [integration-v2-init, posthog-best-practices]\nallowedTools: [Read, Write, Edit, Glob, Grep]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure PostHog is initialized. If the project already has a working\n`posthog.init(...)` (or the framework's equivalent) with its env keys wired,\nleave it alone and say so in your handoff. If it doesn't, create it following\nyour skill — it owns the how: the framework's init point, the env-var wiring\nthrough the wizard tools, and `.env.example`.\n\nYou only exist in this flow because the user asked for Replay vision on a\nrepo without PostHog. Initialize the SDK so recordings can flow and stop —\nno instrumentation, no extras. Don't touch session-recording options either\nway; the enable-replay task after you owns those.\n\n## How you know you succeeded\n\nAn init point exists with the PostHog env keys present — whether it already\ndid or you just created it — keys in the env file, never hardcoded. Your\nhandoff names the files involved and how the client is constructed, so the\nenable-replay task can find the init options without re-discovering them.\n", - install: - "---\ntype: install\nflow: replay-vision\nlabel: Add the PostHog SDK to the manifest\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-haiku-4-5\nskills: [integration-v2-install]\nallowedTools: [Read, Edit, Glob, Grep, Bash]\ndisallowedTools: [enqueue_task]\ndependsOn: []\n---\n\n## Goal\n\nMake sure the PostHog SDK is in the manifest. If it's already installed,\nleave it alone and say so in your handoff. If it isn't, install it following\nyour skill — it owns the how: the package manager rules, the version rules,\nwhat counts as an environment failure, and the fallback.\n\nYou only exist in this flow because the user asked for Replay vision on a\nrepo without PostHog. Install the client library (plus the server library if\nthe app sends events server-side) and stop — no instrumentation, no extras.\n\n## How you know you succeeded\n\nThe SDK is declared in the manifest at a real version — whether it already\nwas or you just installed it — or your handoff plainly says why the\nenvironment stopped you. Your handoff names the manifest and the package, so\nlater steps import it under the name they will actually get.\n", - report: - '---\ntype: report\nflow: replay-vision\nlabel: Report and hand off\nsink: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: []\nallowedTools: [Read, Glob, Grep, Write, posthog_exec]\ndisallowedTools: [enqueue_task]\ndependsOn: [scanner-broken-experiences, scanner-user-frustration, scanner-session-summaries]\n---\n\n## Goal\n\nTell the user what Replay vision is now doing for them, from the handoffs of\nevery task in the run. Briefly and concretely:\n\n- What is now recording (or the follow-up needed to make it record).\n- Each scanner created or updated: its name, what it watches, its query\n scope, and its estimated monthly credit spend.\n- Anything skipped or deferred, with the reason — a skipped scanner on a\n backend-only project is an outcome, not an apology.\n- Where results appear: the Replay vision page in PostHog, with the first\n observations arriving as new recordings complete.\n\nIf the run also installed and initialized the SDK, say so — the user started\nthis command without PostHog and now has it.\n\nWrite the report to `./posthog-replay-vision-report.md` (the wizard shows this\nfile at the end of the run), then publish the same content as your handoff.\n\n## How you know you succeeded\n\nA user who reads only your report knows what watches their product, what it\ncosts, what still needs them, and where to look first.\n', - 'scanner-broken-experiences': - "---\ntype: scanner-broken-experiences\nflow: replay-vision\nlabel: Breakage scanner\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-scanners-core, replay-vision-scanner-broken-experiences]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [enable-replay]\n---\n\n## Goal\n\nCreate the breakage monitor: the product visibly breaking, on the flow where\nbreaking costs the most — named and written for this product. The brief, the\nlocked scaffold, and the create/re-run mechanics are in your skills — you\nfill the blanks from the repo:\n\n- **`name`** — what it watches, in this product's own words.\n- **`query`** — this product's key completion flow plus its immediate\n predecessors. Checkout, signup, booking, publish — whatever this product's\n \"done\" is. Read router files and page/route directories to find the real\n paths; never guess at `/checkout` if this app calls it `/booking`.\n- **`{{WATCH_FOR}}`** — the concrete failure modes of this flow, on screen,\n in the product's vocabulary.\n- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence.\n\nIf there is no identifiable completion flow, don't invent one — fall back to\nthe handful of highest-traffic paths and record in your handoff that you\ncouldn't identify a completion flow. If the enable-replay handoff says this\nproduct has no web surface, skip creation entirely and say why — that is a\ncorrect outcome.\n\n## How you know you succeeded\n\nThe scanner exists in PostHog scoped to this product's real completion flow,\nor your handoff says exactly why it was skipped or deferred.\n", - 'scanner-session-summaries': - "---\ntype: scanner-session-summaries\nflow: replay-vision\nlabel: Session summaries scanner\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: low\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-scanners-core, replay-vision-scanner-session-summaries]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [enable-replay]\n---\n\n## Goal\n\nCreate the summary scanner: a rolling sample of plain-language session\nsummaries in this product's own vocabulary, so the user sees the breadth of\nwhat scanners produce. The brief, the locked scaffold, and the create/re-run\nmechanics are in your skills — you fill the blanks from the repo:\n\n- **`name`** — in this product's own words.\n- **`{{VOCABULARY}}`** — the nouns and verbs this product's UI uses for its\n key flows, so summaries read like the product.\n- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence.\n\nIt samples every recording with no query scope, kept cheap by a low sampling\nrate — **never raise the sampling rate during setup**.\n\nIf the enable-replay handoff says this product has no web surface and no\nrecorded web sessions at all, skip creation and say why — that is a correct\noutcome.\n\n## How you know you succeeded\n\nThe scanner exists in PostHog at the brief's sampling rate, or your\nhandoff says exactly why it was skipped or deferred.\n", - 'scanner-user-frustration': - "---\ntype: scanner-user-frustration\nflow: replay-vision\nlabel: User frustration scanner\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: medium\nskills: [replay-vision-scanners-core, replay-vision-scanner-user-frustration]\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, enqueue_task]\ndependsOn: [enable-replay]\n---\n\n## Goal\n\nCreate the frustration monitor: the user getting stuck, named and written\nfor this product. It is gated on `$rageclick` — cheap and high-precision,\nbecause here the gating event *is* the friction. The brief, the locked\nscaffold, and the create/re-run mechanics are in your skills — you fill the\nblanks from the repo:\n\n- **`name`** — what it watches, in this product's own words.\n- **`{{STUCK_MOMENTS}}`** — where a user of this product realistically gets\n stuck, on screen, in the product's vocabulary.\n- **`{{PRODUCT_CONTEXT}}`** — one plain factual sentence.\n\nIf the enable-replay handoff says this product has no web surface and no\nrecorded web sessions at all, skip creation and say why — that is a correct\noutcome.\n\n## How you know you succeeded\n\nThe scanner exists in PostHog gated on `$rageclick` alone, or your handoff\nsays exactly why it was skipped or deferred.\n", - 'setup-replay-vision': - '---\ntype: setup-replay-vision\nflow: replay-vision\nseed: true\nmodel_pi: openai/gpt-5.6-terra\neffort_pi: medium\nmodel_sdk: claude-sonnet-5\neffort_sdk: high\nskills: []\nallowedTools: [Read, Glob, Grep, posthog_exec]\ndisallowedTools: [Write, Edit, Bash, complete_task]\ndependsOn: []\n---\n\n## Goal\n\nPlan a Replay vision setup and seed the task queue. A scanner is an LLM that\nwatches one session recording at a time and writes an observation — a score, a\ntag, or a summary — and every observation lands as a queryable PostHog event.\nScanners only see what session replay records, so the plan has two halves:\nmake sure recordings flow, then create scanners tailored to this product.\n\nFirst, establish one fact from the repo: **is PostHog already integrated?**\nLook for `posthog-js` or a server SDK in the dependency manifests, or a\n`posthog.init(...)` / snippet in the source. Check the project state for\nexisting events if the repo is ambiguous.\n\nThen seed one of two graphs:\n\n**PostHog is already integrated** — the common case:\n\n- `enable-replay`, with no dependencies.\n- The three scanner tasks — `scanner-broken-experiences`,\n `scanner-user-frustration`, `scanner-session-summaries` — each after\n `enable-replay`, independent of one another, so they run in parallel.\n- `report`, after all three scanner tasks. It writes the handoff last, so it\n describes what actually shipped.\n\n**PostHog is not integrated** — do not stop, integrate:\n\n- `install` and `init`, independent of each other.\n- `enable-replay`, after `install` and `init`.\n- The three scanner tasks and `report`, wired exactly as above.\n\nNever plan an identify, capture, or dashboard task — this run sets up Replay\nvision, not the full integration. The minimal SDK footprint that `install` and\n`init` leave behind is enough for recordings to flow.\n\n## How you know you succeeded\n\nEvery task in the chosen graph is queued with that dependency shape, the\nreport last, and the first task runnable. Keep labels short — the action in a\nfew words.\n', + 'enable-replay': enableReplay, + init, + install, + report, + 'scanner-broken-experiences': scannerBrokenExperiences, + 'scanner-session-summaries': scannerSessionSummaries, + 'scanner-user-frustration': scannerUserFrustration, + 'setup-replay-vision': setupReplayVision, }; diff --git a/tsconfig.base.json b/tsconfig.base.json index edc6f2d1d..df8887156 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -22,7 +22,7 @@ "esModuleInterop": true, "jsx": "react-jsx", "typeRoots": ["./node_modules/@types", "./types"], - "types": ["node", "read-env", "xcode", "vitest-global-types"], + "types": ["node", "read-env", "xcode", "vitest-global-types", "markdown"], // Entries only. A layer reads another through its entry, and the layer's // `references` decide which projects it can reach at all. "paths": { diff --git a/tsdown.config.ts b/tsdown.config.ts index db4192180..74063b7b2 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -58,6 +58,8 @@ export default defineConfig({ platform: 'node', target: 'es2022', fixedExtension: false, + // Programs import their agent prompts (`agent/.md`) as text. + loader: { '.md': 'text' }, // Lock environment variables at build time. // After build, setting NODE_ENV at runtime has zero effect on the wizard. diff --git a/types/markdown.d.ts b/types/markdown.d.ts new file mode 100644 index 000000000..8b120564f --- /dev/null +++ b/types/markdown.d.ts @@ -0,0 +1,5 @@ +// A `.md` import is the file's text (tsdown's loader, the Vitest plugin, `pnpm try`'s hook). +declare module '*.md' { + const text: string; + export default text; +} diff --git a/vitest.config.ts b/vitest.config.ts index 0cf3bffc2..3448d898f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -30,6 +30,17 @@ function resolveTsForJs(): Plugin { }; } +/** Loads a `.md` import as the file's text, as the build's `.md` loader does. */ +function markdownAsText(): Plugin { + return { + name: 'markdown-as-text', + transform(code, id) { + if (!id.split('?')[0].endsWith('.md')) return null; + return { code: `export default ${JSON.stringify(code)};`, map: null }; + }, + }; +} + // Per-surface Vitest projects keyed by today's directories. Each project runs // alone with `vitest run --project `; `vitest run` runs them all. const TESTS = '__tests__/**/*.{js,jsx,ts,tsx}'; @@ -58,7 +69,7 @@ const project = (name: string, include: string[], exclude: string[] = []) => ({ }); export default defineConfig({ - plugins: [resolveTsForJs()], + plugins: [resolveTsForJs(), markdownAsText()], // The source targets the React 19 automatic JSX runtime (tsconfig // `"jsx": "react-jsx"`); mirror that here so the TUI `.tsx` tests transform. esbuild: { jsx: 'automatic' },