From 9ad2691e285a546081c8776745ad294a4afa5f35 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Tue, 1 Sep 2026 18:03:22 +0200 Subject: [PATCH 1/5] feat(error-tracking): add the error-tracking program on the orchestrator flow New `wizard error-tracking` command. Modeled on replay-vision: a detect step carries the framework id into orchestrator preflight, the context-mill `error-tracking` flow seeds install/init (integration-v2 step-skills) when the repo has no PostHog, then exception capture, then the source-map subgraph where the platform needs it. The detect step also pre-installs posthog-cli for symbol-upload platforms, which the agent cannot (warlock blocks npm install -g). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVXmLWB31CBujD2FJ2ogvY --- bin.ts | 2 + e2e-harness/action-registry.ts | 1 + src/commands/error-tracking.ts | 15 ++ .../runner/__tests__/switchboard.test.ts | 12 ++ .../switchboard/flags/__tests__/flags.test.ts | 8 +- src/lib/agent/runner/switchboard/index.ts | 5 + src/lib/oauth/program-scopes.ts | 20 ++ .../programs/__tests__/error-tracking.test.ts | 65 ++++++ src/lib/programs/error-tracking/index.ts | 204 ++++++++++++++++++ src/lib/programs/program-registry.ts | 3 + src/ui/tui/screen-registry.tsx | 2 + src/ui/tui/screen-sequences.ts | 1 + .../tui/screens/ErrorTrackingIntroScreen.tsx | 92 ++++++++ 13 files changed, 426 insertions(+), 4 deletions(-) create mode 100644 src/commands/error-tracking.ts create mode 100644 src/lib/programs/__tests__/error-tracking.test.ts create mode 100644 src/lib/programs/error-tracking/index.ts create mode 100644 src/ui/tui/screens/ErrorTrackingIntroScreen.tsx diff --git a/bin.ts b/bin.ts index f4bb1bd58..2c518a703 100644 --- a/bin.ts +++ b/bin.ts @@ -66,6 +66,7 @@ import { warehouseCommand } from './src/commands/warehouse'; import { selfDrivingCommand } from './src/commands/self-driving'; import { slackCommand } from './src/commands/slack'; import { uploadSourcemapsCommand } from './src/commands/upload-sourcemaps'; +import { errorTrackingCommand } from './src/commands/error-tracking'; import { skillCommand } from './src/commands/skill'; import { cliCommand } from './src/commands/cli'; import { recoverOrphanedSettingsBackups } from './src/lib/agent/claude-settings'; @@ -100,5 +101,6 @@ Wizard.use(basicIntegrationCommand) .use(selfDrivingCommand) .use(slackCommand) .use(uploadSourcemapsCommand) + .use(errorTrackingCommand) .use(skillCommand) .init(); diff --git a/e2e-harness/action-registry.ts b/e2e-harness/action-registry.ts index 609432786..25585b2dd 100644 --- a/e2e-harness/action-registry.ts +++ b/e2e-harness/action-registry.ts @@ -104,6 +104,7 @@ export const ACTION_REGISTRY: Partial> = { [ScreenId.AgentSkillIntro]: [confirmSetupAction], [ScreenId.AiObservabilityIntro]: [confirmSetupAction], [ScreenId.MetricsIntro]: [confirmSetupAction], + [ScreenId.ErrorTrackingIntro]: [confirmSetupAction], [ScreenId.AuditIntro]: [confirmSetupAction], [ScreenId.DoctorIntro]: [confirmSetupAction], [ScreenId.WarehouseIntro]: [confirmSetupAction], diff --git a/src/commands/error-tracking.ts b/src/commands/error-tracking.ts new file mode 100644 index 000000000..3db0b2816 --- /dev/null +++ b/src/commands/error-tracking.ts @@ -0,0 +1,15 @@ +import { errorTrackingConfig } from '@lib/programs/error-tracking/index'; + +import type { Command } from './command'; +import { nativeCommandFactory } from './factories/native-command-factory'; + +/** + * `wizard error-tracking` — flat skill command, set up error tracking today. + * + * Wires up exception capture and — where the platform needs it — source-map / + * debug-symbol upload. Runs the `error-tracking` orchestrator flow, which + * reuses the integration-v2 install/init mini-agents when the repo has no + * PostHog integration yet, so it works on uninstrumented projects too. + */ +export const errorTrackingCommand: Command = + nativeCommandFactory(errorTrackingConfig); diff --git a/src/lib/agent/runner/__tests__/switchboard.test.ts b/src/lib/agent/runner/__tests__/switchboard.test.ts index aa925c6dc..5b495d9ac 100644 --- a/src/lib/agent/runner/__tests__/switchboard.test.ts +++ b/src/lib/agent/runner/__tests__/switchboard.test.ts @@ -64,6 +64,7 @@ describe('switchboard PROGRAM_BINDINGS', () => { if (program === 'error-tracking-upload-source-maps') continue; // pinned below if (program === 'metrics') continue; // pinned below if (program === 'replay-vision') continue; // pinned below + if (program === 'error-tracking') continue; // pinned below expect(resolveBinding({ program, flags: {} })).toEqual(DEFAULT_RESOLVED); } }); @@ -113,6 +114,17 @@ describe('switchboard PROGRAM_BINDINGS', () => { }, trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, }, + { + name: 'binds error-tracking to the orchestrator sequence', + ctx: { program: 'error-tracking', flags: {} }, + binding: { + sequence: Sequence.orchestrator, + harness: Harness.anthropic, + model: DEFAULT_AGENT_MODEL, + thinkingLevel: undefined, + }, + trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, + }, { name: 'falls back to DEFAULT_BINDING for an unmapped program', ctx: { program: 'not-a-program', flags: {} }, diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts index 16383d72a..00f129910 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -292,10 +292,10 @@ describe('isolation — everything on at once', () => { model: GPT5_6_SOL_MODEL, thinkingLevel: 'medium', }); - } else if (program === 'replay-vision') { - // Orchestrator from its OWN binding, not the flag — the - // wizard-orchestrator experiment does not cover this program, so it - // lands here whether the flag is on or off. Anthropic, not pi. + } else if (program === 'replay-vision' || program === 'error-tracking') { + // Orchestrator from their OWN bindings, not the flag — the + // wizard-orchestrator experiment does not cover these programs, so + // they land here whether the flag is on or off. Anthropic, not pi. expect(resolved).toEqual({ ...LINEAR_ANTHROPIC_DEFAULT, sequence: Sequence.orchestrator, diff --git a/src/lib/agent/runner/switchboard/index.ts b/src/lib/agent/runner/switchboard/index.ts index fb4232036..61cbf4b69 100644 --- a/src/lib/agent/runner/switchboard/index.ts +++ b/src/lib/agent/runner/switchboard/index.ts @@ -154,6 +154,11 @@ export const PROGRAM_BINDINGS: Partial> = { harness: Harness.anthropic, model: DEFAULT_AGENT_MODEL, }, + 'error-tracking': { + sequence: Sequence.orchestrator, + harness: Harness.anthropic, + model: DEFAULT_AGENT_MODEL, + }, 'ai-observability': { sequence: Sequence.linear, harness: Harness.anthropic, diff --git a/src/lib/oauth/program-scopes.ts b/src/lib/oauth/program-scopes.ts index 69da4f59a..3e3c25686 100644 --- a/src/lib/oauth/program-scopes.ts +++ b/src/lib/oauth/program-scopes.ts @@ -250,6 +250,25 @@ export const REPLAY_VISION_SCOPE_ADDITIONS = [ 'replay_scanner:write', ] as const; +/** + * Extra scopes the error-tracking program needs on top of + * `WIZARD_OAUTH_SCOPES`. + * • error_tracking:read — the flow's report task reads symbol sets and + * issue state through `posthog_exec` to tell the user where to verify + * that captured exceptions actually land. + * • product_enablement:write — the report task turns on the Error Tracking + * product (`products-enable`) so the captured exceptions have a UI to + * land in; the server owns the enable recipe. + * + * No OAuth-ceiling edit needed — both are unprivileged public scope objects + * covered by the apps' `@default` sentinel, and self-driving already requests + * both. + */ +export const ERROR_TRACKING_SCOPE_ADDITIONS = [ + 'error_tracking:read', + 'product_enablement:write', +] as const; + /** * Per-program scope additions, layered on top of `WIZARD_OAUTH_SCOPES`. * @@ -280,6 +299,7 @@ const PROGRAM_SCOPE_ADDITIONS: Partial> = { ], slack: CONNECT_SLACK_SCOPE_ADDITIONS, 'replay-vision': REPLAY_VISION_SCOPE_ADDITIONS, + 'error-tracking': ERROR_TRACKING_SCOPE_ADDITIONS, }; /** diff --git a/src/lib/programs/__tests__/error-tracking.test.ts b/src/lib/programs/__tests__/error-tracking.test.ts new file mode 100644 index 000000000..819e07640 --- /dev/null +++ b/src/lib/programs/__tests__/error-tracking.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from 'vitest'; + +import { Integration } from '@lib/constants'; +import { + errorTrackingConfig, + SYMBOL_UPLOAD_CLI_FRAMEWORKS, +} from '@lib/programs/error-tracking/index'; + +describe('error-tracking program', () => { + test('runs the error-tracking agent flow', () => { + expect(errorTrackingConfig.agentFlow).toBe('error-tracking'); + }); + + test('detects the framework before the agent-skill steps', () => { + expect(errorTrackingConfig.steps[0]?.id).toBe('detect'); + expect(errorTrackingConfig.steps[0]?.onReady).toBeDefined(); + }); + + test('declares ci prerequisite work for headless runs', () => { + expect(errorTrackingConfig.ciPreRun).toBeDefined(); + }); + + test('shows the program-specific intro screen', () => { + const intro = errorTrackingConfig.steps.find((s) => s.id === 'intro'); + expect(intro?.screenId).toBe('error-tracking-intro'); + }); + + test('pre-installs no skill — the flow resolves variants per framework', () => { + // There is no bare `error-tracking` menu entry; a seeded skillId would + // send the linear path to a skill-not-found abort and mislead the intro. + expect(errorTrackingConfig.skillId).toBeUndefined(); + expect(errorTrackingConfig.run).toBeDefined(); + if ( + errorTrackingConfig.run && + typeof errorTrackingConfig.run !== 'function' + ) { + expect(errorTrackingConfig.run.skillId).toBeUndefined(); + } + }); +}); + +describe('error-tracking posthog-cli pre-install set', () => { + test('contains only real Integration values', () => { + for (const integration of SYMBOL_UPLOAD_CLI_FRAMEWORKS) { + expect(Object.values(Integration)).toContain(integration); + } + }); + + test('covers the symbol-upload platforms and no web ones', () => { + // Keep in lockstep with VARIANTS_REQUIRING_POSTHOG_CLI in the source-maps + // program: their builds shell out to a machine-global posthog-cli. + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.swift)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.android)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.reactNative)).toBe( + true, + ); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.flutter)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.go)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.rust)).toBe(true); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.nextjs)).toBe(false); + expect(SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(Integration.javascript_web)).toBe( + false, + ); + }); +}); diff --git a/src/lib/programs/error-tracking/index.ts b/src/lib/programs/error-tracking/index.ts new file mode 100644 index 000000000..2a32f2314 --- /dev/null +++ b/src/lib/programs/error-tracking/index.ts @@ -0,0 +1,204 @@ +import { Integration } from '@lib/constants'; +import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; +import { scopeInstallDirToProject } from '@lib/detection/project-scope'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { getContentBlocks } from '@lib/programs/agent-skill/content/index'; +import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/steps'; +import { detectPostHogIntegration } from '@lib/programs/posthog-integration/detect'; +import type { + ProgramConfig, + ProgramReadyContext, + ProgramStep, +} from '@lib/programs/program-step'; +import type { WizardSession } from '@lib/wizard-session'; +import { installOrUpdatePostHogCli } from '@steps/install-cli-steering'; +import { getUI } from '@ui'; +import { analytics } from '@utils/analytics'; +import { wizardAbort } from '@utils/wizard-abort'; +import { ErrorCodes } from '@lib/errors'; + +const ERROR_TRACKING_REPORT_FILE = 'posthog-error-tracking-report.md'; +const ERROR_TRACKING_DOCS_URL = 'https://posthog.com/docs/error-tracking'; + +/** + * Frameworks whose symbol upload shells out to a machine-global `posthog-cli` + * with no npx / local-dep fallback. The wizard pre-installs the CLI for them + * because warlock blocks the agent's `npm install -g`. Mirrors + * `VARIANTS_REQUIRING_POSTHOG_CLI` in the source-maps program, but keyed by + * wizard `Integration` because here the framework is known before the flow's + * seed picks an uploader variant (`swift` maps to the `ios` uploader). + */ +export const SYMBOL_UPLOAD_CLI_FRAMEWORKS: ReadonlySet = new Set([ + Integration.swift, + Integration.android, + Integration.reactNative, + Integration.flutter, + Integration.go, + Integration.rust, +]); + +let postHogCliInstallAttempted = false; + +/** + * Pre-install posthog-cli when the detected framework's symbol upload will + * shell out to it. Warn, don't fail — the run still instruments exception + * capture; only the release build's upload step needs the CLI. + */ +function maybePreinstallPostHogCli(integration: Integration): void { + if (!SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(integration)) return; + if (postHogCliInstallAttempted) return; + postHogCliInstallAttempted = true; + + const result = installOrUpdatePostHogCli(); + if (!result.success) { + analytics.wizardCapture('error tracking posthog-cli preinstall failed', { + integration, + error: String(result.error).slice(0, 500), + }); + analytics.captureException( + result.errorObject ?? + new Error(`posthog-cli pre-install failed: ${result.error}`), + { source: 'error_tracking_cli_preinstall', integration }, + ); + getUI().log.warn( + `Could not pre-install posthog-cli (${result.error}). Your release build ` + + `will fail to upload debug symbols until it's installed: npm install -g @posthog/cli@latest`, + ); + } +} + +/** + * Framework detection ahead of the run, exactly like the default integration + * program. The orchestrator requires it: `session.skillId` must hold the + * detected framework id before the run arm starts, because the runner + * resolves the reference integration skill and every task's mini-skill + * variants (`integration-v2-install`, `integration-v2-error-tracking-step`, …) + * against it in preflight. Unlike replay-vision there is no platform + * allow-list — every detectable framework has an error-tracking-step variant. + */ +const DETECT_STEP: ProgramStep = { + id: 'detect', + label: 'Detecting framework', + onReady: async (ctx: ProgramReadyContext) => { + const integration = await detectFramework(ctx.session.installDir); + if (integration) maybePreinstallPostHogCli(integration); + await detectPostHogIntegration(ctx); + }, +}; + +const ERROR_TRACKING_STEPS: ProgramStep[] = [ + DETECT_STEP, + ...AGENT_SKILL_STEPS.map((step) => + step.id === 'intro' ? { ...step, screenId: 'error-tracking-intro' } : step, + ), +]; + +/** + * Mode-agnostic run instructions. The orchestrator's seed reads them as + * context on top of its own flow prompts; a linear override + * (`--sequence=linear`) relies on them entirely, so they spell out the + * skill-menu lookups the flow's tasks would otherwise perform. + */ +const ERROR_TRACKING_PROMPT = `Set up PostHog error tracking end-to-end: + +1. If PostHog is not integrated yet, install and initialize the SDK first — + do not abort. Pick the matching variant from the skill menu's + "integration-v2/install" and "integration-v2/init" categories. + +2. Wire up exception capture: install the "error-tracking" skill variant that + matches this project's platform (\`load_skill_menu\` with + \`category: "error-tracking"\`) and follow it. Set capture up in one place — + the SDK's own mechanism, never manual capture calls sprinkled across files. + +3. When the platform ships minified bundles or stripped binaries (browser JS, + React Native, iOS, Android, Flutter, Go, Rust), wire up source-map / + debug-symbol upload too: install the matching + "error-tracking-upload-source-maps" skill variant and follow it, including + credentials and CI. Skip this step on platforms with readable stack traces + (plain Python, Ruby, PHP, Elixir, JVM servers). + +The final report is written to ./${ERROR_TRACKING_REPORT_FILE}.`; + +/** + * `wizard error-tracking` — flat command on the orchestrator sequence. + * + * Makes uncaught errors reach PostHog with readable stack traces. The + * orchestrator runs the `error-tracking` agent flow (context-mill + * `context/agents/error-tracking`): the seed enqueues the install/init tasks + * (sharing integration-v2's step-skills, like replay-vision) when the project + * has no PostHog yet, then exception capture, then — when the platform needs + * it — the source-map subgraph adapted from the standalone + * `upload-source-maps` flow. + * + * Departures from a plain `createSkillProgram`: + * - No `run.skillId`: the flow's tasks resolve per-framework mini-skills + * themselves (there is no bare `error-tracking` menu entry), so the intro is + * a custom screen rather than the generic skill intro. + * - `DETECT_STEP` in front, so `session.skillId` carries the framework id the + * orchestrator's preflight resolves reference + mini-skill variants with. It + * also pre-installs posthog-cli for symbol-upload platforms, which the agent + * cannot (warlock blocks \`npm install -g\`). + * - `agentFlow` pinned (the id would default to the same value — explicit so + * renaming the program can't silently detach the flow). + * - `ciPreRun` mirrors replay-vision: scope the install dir to the right + * project (monorepos), then detect the framework — the headless equivalent + * of the detect step's onReady hook. + */ +export const errorTrackingConfig: ProgramConfig = { + command: 'error-tracking', + description: 'Set up PostHog error tracking, source-map upload included', + id: 'error-tracking', + agentFlow: 'error-tracking', + steps: ERROR_TRACKING_STEPS, + reportFile: ERROR_TRACKING_REPORT_FILE, + getContentBlocks, + + run: { + integrationLabel: 'error-tracking', + customPrompt: () => ERROR_TRACKING_PROMPT, + successMessage: `Error tracking configured! View the report at ./${ERROR_TRACKING_REPORT_FILE}`, + reportFile: ERROR_TRACKING_REPORT_FILE, + docsUrl: ERROR_TRACKING_DOCS_URL, + spinnerMessage: 'Setting up error tracking...', + estimatedDurationMinutes: 8, + // The flow can park on wizard_ask while the user does slow work (mint a + // personal API key in the browser, run a build and trigger the test + // error). The orchestrator caps per-task asks itself; this covers the + // linear fallback. + askTimeoutMs: 30 * 60 * 1000, + }, + + ciPreRun: async (session: WizardSession): Promise => { + await scopeInstallDirToProject(session); + + const integration = await detectFramework(session.installDir); + if (!integration) { + await wizardAbort({ + code: ErrorCodes.DetectNoFramework, + message: 'Could not auto-detect your framework for this project.', + }); + return; + } + maybePreinstallPostHogCli(integration); + session.integration = integration; + analytics.setTag('integration', integration); + + const frameworkConfig = FRAMEWORK_REGISTRY[integration]; + session.frameworkConfig = frameworkConfig; + session.skillId = integration; + + const context = await gatherFrameworkContext(frameworkConfig, { + installDir: session.installDir, + debug: session.debug, + signup: session.signup, + ci: true, + benchmark: session.benchmark, + yaraReport: session.yaraReport, + }); + for (const [key, value] of Object.entries(context)) { + if (!(key in session.frameworkContext)) { + session.frameworkContext[key] = value; + } + } + }, +}; diff --git a/src/lib/programs/program-registry.ts b/src/lib/programs/program-registry.ts index 2633c295e..7e3156948 100644 --- a/src/lib/programs/program-registry.ts +++ b/src/lib/programs/program-registry.ts @@ -21,6 +21,7 @@ import { posthogDoctorConfig } from './posthog-doctor/index.js'; import { webAnalyticsDoctorConfig } from './web-analytics-doctor/index.js'; import { migrationConfig } from './migration/index.js'; import { errorTrackingUploadSourceMapsConfig } from './error-tracking-upload-source-maps/index.js'; +import { errorTrackingConfig } from './error-tracking/index.js'; import { selfDrivingConfig } from './self-driving/index.js'; import { AGENT_SKILL_STEPS } from './agent-skill/index.js'; import { getContentBlocks as agentSkillContentBlocks } from './agent-skill/content/index.js'; @@ -70,6 +71,7 @@ export const PROGRAM_REGISTRY = [ revenueAnalyticsConfig, warehouseSourceConfig, errorTrackingUploadSourceMapsConfig, + errorTrackingConfig, auditConfig, eventsAuditConfig, posthogDoctorConfig, @@ -97,6 +99,7 @@ export const Program = { RevenueAnalyticsSetup: revenueAnalyticsConfig.id, WarehouseSource: warehouseSourceConfig.id, ErrorTrackingUploadSourceMaps: errorTrackingUploadSourceMapsConfig.id, + ErrorTracking: errorTrackingConfig.id, Migration: migrationConfig.id, Audit: auditConfig.id, EventsAudit: eventsAuditConfig.id, diff --git a/src/ui/tui/screen-registry.tsx b/src/ui/tui/screen-registry.tsx index 42765deec..1b18a3d88 100644 --- a/src/ui/tui/screen-registry.tsx +++ b/src/ui/tui/screen-registry.tsx @@ -30,6 +30,7 @@ import { SourceMapsOutroScreen } from './screens/SourceMapsOutroScreen.js'; import { AgentSkillIntroScreen } from './screens/AgentSkillIntroScreen.js'; import { AiObservabilityIntroScreen } from './screens/AiObservabilityIntroScreen.js'; import { MetricsIntroScreen } from './screens/MetricsIntroScreen.js'; +import { ErrorTrackingIntroScreen } from './screens/ErrorTrackingIntroScreen.js'; import { SelfDrivingIntroScreen } from './screens/SelfDrivingIntroScreen.js'; import { SelfDrivingIntegrationCheckScreen } from './screens/SelfDrivingIntegrationCheckScreen.js'; import { SelfDrivingIntegrationDetectScreen } from './screens/SelfDrivingIntegrationDetectScreen.js'; @@ -95,6 +96,7 @@ export function createScreens( ), [ScreenId.MetricsIntro]: , + [ScreenId.ErrorTrackingIntro]: , [ScreenId.SelfDrivingIntro]: , [ScreenId.SelfDrivingIntegrationCheck]: ( diff --git a/src/ui/tui/screen-sequences.ts b/src/ui/tui/screen-sequences.ts index 8131b3f7f..1da8347c0 100644 --- a/src/ui/tui/screen-sequences.ts +++ b/src/ui/tui/screen-sequences.ts @@ -26,6 +26,7 @@ export enum ScreenId { AgentSkillIntro = 'agent-skill-intro', AiObservabilityIntro = 'ai-observability-intro', MetricsIntro = 'metrics-intro', + ErrorTrackingIntro = 'error-tracking-intro', SelfDrivingIntro = 'self-driving-intro', SelfDrivingIntegrationCheck = 'self-driving-integration-check', SelfDrivingIntegrationDetect = 'self-driving-integration-detect', diff --git a/src/ui/tui/screens/ErrorTrackingIntroScreen.tsx b/src/ui/tui/screens/ErrorTrackingIntroScreen.tsx new file mode 100644 index 000000000..cceb74362 --- /dev/null +++ b/src/ui/tui/screens/ErrorTrackingIntroScreen.tsx @@ -0,0 +1,92 @@ +import { Box, Text } from 'ink'; +import { useState, useSyncExternalStore } from 'react'; +import type { WizardStore } from '@ui/tui/store'; +import { IntroScreenLayout } from '@ui/tui/screens/IntroScreenLayout'; +import { + SkillSourceInfo, + useSkillEntry, +} from '@ui/tui/screens/SkillSourceInfo'; + +interface ErrorTrackingIntroScreenProps { + store: WizardStore; +} + +export const ErrorTrackingIntroScreen = ({ + store, +}: ErrorTrackingIntroScreenProps) => { + useSyncExternalStore( + (cb) => store.subscribe(cb), + () => store.getSnapshot(), + ); + + const [showingMoreInfo, setShowingMoreInfo] = useState(false); + const { session } = store; + // error-tracking resolves its skill variants per framework at run time, so + // there's no single pre-seeded skillId. Fall back to the group id for the + // "more info" lookup. + const skillId = session.skillId ?? 'error-tracking'; + const { skillEntry, fetchFailed } = useSkillEntry(skillId, session.localMcp); + + const body = showingMoreInfo ? ( + + + + The wizard is an agent that executes PostHog tasks. Its code is open + source: https://github.com/PostHog/wizard + + + + + The{' '} + + error-tracking + {' '} + program makes uncaught errors reach PostHog with readable stack traces. + It installs and initializes the PostHog SDK when the project doesn't + have it yet, wires up exception capture through the SDK's own mechanism, + and — on platforms that ship minified bundles or stripped binaries — + sets up source-map / debug-symbol upload for your production builds. + + + + + + ) : ( + + + Let's make uncaught errors reach PostHog with readable stack traces. + + + ); + + const menuOptions = showingMoreInfo + ? [{ label: 'Back', value: 'back' }] + : [ + { label: 'Continue', value: 'continue' }, + { label: 'More info', value: 'more-info' }, + { label: 'Cancel', value: 'cancel' }, + ]; + + const handleSelect = (value: string) => { + if (value === 'cancel') process.exit(0); + else if (value === 'more-info') setShowingMoreInfo(true); + else if (value === 'back') setShowingMoreInfo(false); + else store.completeSetup(); + }; + + return ( + + ); +}; From 10cf2dcf7c5ed03bdcd0ca177e5653897402565b Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Tue, 1 Sep 2026 18:21:25 +0200 Subject: [PATCH 2/5] feat(error-tracking): route the program to the orchestrator on pi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Binding harness anthropic→pi, like metrics: the binding routes only, and every stage's model and effort are pinned context-mill side in the flow's frontmatter (terra seed, sol tasks, luna report). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVXmLWB31CBujD2FJ2ogvY --- src/lib/agent/runner/__tests__/switchboard.test.ts | 6 +++--- .../switchboard/flags/__tests__/flags.test.ts | 14 +++++++------- src/lib/agent/runner/switchboard/index.ts | 5 ++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/lib/agent/runner/__tests__/switchboard.test.ts b/src/lib/agent/runner/__tests__/switchboard.test.ts index 5b495d9ac..23cf03315 100644 --- a/src/lib/agent/runner/__tests__/switchboard.test.ts +++ b/src/lib/agent/runner/__tests__/switchboard.test.ts @@ -115,11 +115,11 @@ describe('switchboard PROGRAM_BINDINGS', () => { trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, }, { - name: 'binds error-tracking to the orchestrator sequence', + name: 'binds error-tracking to the orchestrator on pi; stage models come from the flow frontmatter', ctx: { program: 'error-tracking', flags: {} }, binding: { sequence: Sequence.orchestrator, - harness: Harness.anthropic, + harness: Harness.pi, model: DEFAULT_AGENT_MODEL, thinkingLevel: undefined, }, @@ -244,7 +244,7 @@ describe('switchboard composed clamp', () => { model: GPT5_6_SOL_MODEL, thinkingLevel: 'medium', } - : program === 'metrics' + : program === 'metrics' || program === 'error-tracking' ? { ...DEFAULT_RESOLVED, harness: Harness.pi } : DEFAULT_RESOLVED, ); diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts index 00f129910..04a628ba7 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -278,9 +278,9 @@ describe('isolation — everything on at once', () => { ...LINEAR_ANTHROPIC_DEFAULT, model: SONNET_5_MODEL, }); - } else if (program === 'metrics') { - // Orchestrator + pi from its OWN binding, not the flag; stage models - // are pinned context-mill side in the flow frontmatter. + } else if (program === 'metrics' || program === 'error-tracking') { + // Orchestrator + pi from their OWN bindings, not the flag; stage + // models are pinned context-mill side in the flow frontmatter. expect(resolved).toEqual({ ...ORCHESTRATOR_PI_DEFAULT, }); @@ -292,10 +292,10 @@ describe('isolation — everything on at once', () => { model: GPT5_6_SOL_MODEL, thinkingLevel: 'medium', }); - } else if (program === 'replay-vision' || program === 'error-tracking') { - // Orchestrator from their OWN bindings, not the flag — the - // wizard-orchestrator experiment does not cover these programs, so - // they land here whether the flag is on or off. Anthropic, not pi. + } else if (program === 'replay-vision') { + // Orchestrator from its OWN binding, not the flag — the + // wizard-orchestrator experiment does not cover this program, so it + // lands here whether the flag is on or off. Anthropic, not pi. expect(resolved).toEqual({ ...LINEAR_ANTHROPIC_DEFAULT, sequence: Sequence.orchestrator, diff --git a/src/lib/agent/runner/switchboard/index.ts b/src/lib/agent/runner/switchboard/index.ts index 61cbf4b69..f8c183ae4 100644 --- a/src/lib/agent/runner/switchboard/index.ts +++ b/src/lib/agent/runner/switchboard/index.ts @@ -154,9 +154,12 @@ export const PROGRAM_BINDINGS: Partial> = { harness: Harness.anthropic, model: DEFAULT_AGENT_MODEL, }, + // Orchestrator on pi, like metrics. The binding routes only; every stage's + // model and effort are pinned context-mill side in the flow's frontmatter + // (`model_pi`/`effort_pi`: terra seed, sol tasks, luna report). 'error-tracking': { sequence: Sequence.orchestrator, - harness: Harness.anthropic, + harness: Harness.pi, model: DEFAULT_AGENT_MODEL, }, 'ai-observability': { From 308374b564d87860a2423ed7644e961708a02a03 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Tue, 1 Sep 2026 18:52:35 +0200 Subject: [PATCH 3/5] feat(error-tracking): make the program e2e-drivable e2e.json flow definition + profile registration: the driver confirms the intro, answers the flow's api-key ask from SOURCE_MAPS_CLI_KEY (secret rule, vaulted by the ask tool) and declines the test-affordance offer. Two variations: the pinned orchestrator-on-pi binding and the linear fallback. Unlocks snapshots mode and the workbench e2e runner for the command. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVXmLWB31CBujD2FJ2ogvY --- e2e-harness/profiles.ts | 3 ++ src/lib/programs/error-tracking/test/e2e.json | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/lib/programs/error-tracking/test/e2e.json diff --git a/e2e-harness/profiles.ts b/e2e-harness/profiles.ts index c846ea2b3..9134faba3 100644 --- a/e2e-harness/profiles.ts +++ b/e2e-harness/profiles.ts @@ -25,6 +25,7 @@ import metricsE2e from '@lib/programs/metrics/test/e2e.json'; import replayVisionE2e from '@lib/programs/replay-vision/test/e2e.json'; import selfDrivingE2e from '@lib/programs/self-driving/test/e2e.json'; import sourceMapsE2e from '@lib/programs/error-tracking-upload-source-maps/test/e2e.json'; +import errorTrackingE2e from '@lib/programs/error-tracking/test/e2e.json'; import warehouseSourceE2e from '@lib/programs/warehouse-source/test/e2e.json'; const PROFILES: Partial> = { @@ -36,6 +37,7 @@ const PROFILES: Partial> = { [Program.SelfDriving]: selfDrivingE2e.profile as WizardE2eProfile, [Program.ErrorTrackingUploadSourceMaps]: sourceMapsE2e.profile as WizardE2eProfile, + [Program.ErrorTracking]: errorTrackingE2e.profile as WizardE2eProfile, [Program.WarehouseSource]: warehouseSourceE2e.profile as WizardE2eProfile, }; @@ -46,6 +48,7 @@ const VARIATIONS: Partial> = { aiObservabilityE2e.variations as WizardE2eVariation[], [Program.Metrics]: metricsE2e.variations as WizardE2eVariation[], [Program.ReplayVision]: replayVisionE2e.variations as WizardE2eVariation[], + [Program.ErrorTracking]: errorTrackingE2e.variations as WizardE2eVariation[], [Program.WarehouseSource]: warehouseSourceE2e.variations as WizardE2eVariation[], }; diff --git a/src/lib/programs/error-tracking/test/e2e.json b/src/lib/programs/error-tracking/test/e2e.json new file mode 100644 index 000000000..454e873e1 --- /dev/null +++ b/src/lib/programs/error-tracking/test/e2e.json @@ -0,0 +1,53 @@ +{ + "program": "error-tracking", + "summary": "Happy path: detect framework, confirm intro, run the error-tracking orchestrator flow (install/init when PostHog is absent, exception capture, the source-map subgraph where the platform needs it, report), delete installed skills. The driver answers the flow's asks: the upload key via wizard_ask (id \"api-key\"; the raw key is vaulted by the ask tool, the agent only sees a secretRef) and declines the test-affordance offer.", + "profile": { + "setup": "first", + "healthCheck": "dismiss", + "mcp": "skip", + "slack": "skip", + "skills": "delete", + "ask": "first", + "askAnswers": [ + { "match": "api-key", "value": "${SOURCE_MAPS_CLI_KEY}", "secret": true }, + { "match": "test-affordance", "value": "no" } + ] + }, + "variations": [ + { + "name": "default", + "summary": "orchestrator / pi — the program's pinned binding; stage models from the flow frontmatter (terra seed, sol tasks, luna report)" + }, + { + "name": "linear-fallback", + "summary": "linear arm: the customPrompt drives a single agent through menu-installed skills", + "sequence": "linear" + } + ], + "path": [ + { + "screen": "detect", + "auto": "(headless) — framework detection puts the framework id on session.skillId; pre-installs posthog-cli for symbol-upload platforms" + }, + { + "screen": "error-tracking-intro", + "auto": "confirm & continue" + }, + { + "screen": "auth", + "auto": "(external) — the runner resolves credentials from the phx key" + }, + { + "screen": "run", + "auto": "(external) — the orchestrator flow; the profile answers wizard_ask \"api-key\" with SOURCE_MAPS_CLI_KEY and \"test-affordance\" with \"no\"" + }, + { + "screen": "outro", + "auto": "dismiss" + }, + { + "screen": "keep-skills", + "auto": "delete the installed skills" + } + ] +} From 0d6407e40c51359c6d3e1105b6acb3cb71118618 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 2 Sep 2026 13:33:06 +0200 Subject: [PATCH 4/5] fix(e2e): drive the error-tracking intro in decideE2eAction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The screen was registered in the action registry (satisfying the exhaustiveness test) but decideE2eAction keeps its own intro switch, and an unlisted screen falls through to wait — the driver sat on the intro until the host died. Adds the missing case. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVXmLWB31CBujD2FJ2ogvY --- e2e-harness/e2e-profile.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e-harness/e2e-profile.ts b/e2e-harness/e2e-profile.ts index 4ce08f2bb..c5a042fa2 100644 --- a/e2e-harness/e2e-profile.ts +++ b/e2e-harness/e2e-profile.ts @@ -283,6 +283,7 @@ export function decideE2eAction( case ScreenId.AgentSkillIntro: case ScreenId.AiObservabilityIntro: case ScreenId.MetricsIntro: + case ScreenId.ErrorTrackingIntro: case ScreenId.AuditIntro: case ScreenId.SourceMapsIntro: case ScreenId.DoctorIntro: From aaf908cd967131fdf9101434617c569c3ebd6241 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 2 Sep 2026 13:37:39 +0200 Subject: [PATCH 5/5] fix(e2e): initialize the local-dev singleton in the tui-host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bin resolves --local-* flags in its yargs middleware into the local-dev singleton that getSkillsBaseUrl() reads. The tui-host bypasses yargs, so its env-backed local flags reached the session but never the singleton — agent prompts always resolved to production, and an unpublished flow died in preflight with "No seed agent prompt". Initialize the singleton from the same env spellings before the store starts. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVXmLWB31CBujD2FJ2ogvY --- scripts/tui-host.no-jest.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 7d3da90d0..5a77c6e4d 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -25,6 +25,7 @@ import { } from '@lib/programs/program-registry'; import type { Harness, Sequence } from '@lib/constants'; import { buildSession } from '@lib/wizard-session'; +import { initLocalDev } from '@lib/local-dev'; import { runAgent } from '@lib/agent/agent-runner'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { getOrAskForProjectData } from '@utils/setup-utils'; @@ -195,6 +196,17 @@ async function main() { // requires-interactive-mode the moment they need to ask a question. process.env.WIZARD_ASK_AUTODRIVE = '1'; + // The bin initializes the local-dev singleton from its yargs middleware; + // this host bypasses yargs, so `getSkillsBaseUrl()` would silently resolve + // to production even when the session carries the local flags. Initialize it + // here from the same env-backed spellings, before anything reads it. + initLocalDev({ + localDev: process.env.POSTHOG_WIZARD_LOCAL_DEV === 'true', + localMcp: envFlag('POSTHOG_WIZARD_LOCAL_MCP'), + localContextMill: envFlag('POSTHOG_WIZARD_LOCAL_CONTEXT_MILL'), + localPosthog: envFlag('POSTHOG_WIZARD_LOCAL_POSTHOG'), + }); + const { store } = startTUI(VERSION, programId); store.session = buildSession({ installDir: process.env.APP_DIR!,