From d1538cab49420e38bcd59ea86d5d04173423729e Mon Sep 17 00:00:00 2001 From: leonhardprinz Date: Thu, 14 May 2026 10:11:43 -0400 Subject: [PATCH 1/8] feat: add audit-3000 wizard command Adds a new `audit-3000` CLI subcommand that runs the v3000 audit skill (extends the base audit with event quality, stale-flag hygiene, customer enrichment, and use-case match). - New workflow at src/lib/workflows/audit-3000/ that seeds 15 ledger checks (10 base + 5 v3000-specific) and writes to posthog-audit-3000-report.md - New intro screen at src/ui/tui/screens/audit-3000/ - Run + outro screens reused from audit/ and parameterized via a new top-level WorkflowConfig.reportFile field so they render the correct report path for either workflow - Outro success view now explicitly shows the full saved report path Generated-By: PostHog Code Task-Id: 1f2601d8-e5b5-434e-9ef7-29d996a2cdb9 --- src/lib/workflows/agent-skill/index.ts | 1 + src/lib/workflows/audit-3000/index.ts | 127 ++++++++++++++++++ src/lib/workflows/workflow-registry.ts | 2 + src/lib/workflows/workflow-step.ts | 6 + src/ui/tui/flows.ts | 2 + src/ui/tui/screen-registry.tsx | 2 + .../audit-3000/Audit3000IntroScreen.tsx | 89 ++++++++++++ src/ui/tui/screens/audit/AuditOutroScreen.tsx | 19 ++- src/ui/tui/screens/audit/AuditRunScreen.tsx | 5 +- 9 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/lib/workflows/audit-3000/index.ts create mode 100644 src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx diff --git a/src/lib/workflows/agent-skill/index.ts b/src/lib/workflows/agent-skill/index.ts index 2ad4a2ac..ba4cb33c 100644 --- a/src/lib/workflows/agent-skill/index.ts +++ b/src/lib/workflows/agent-skill/index.ts @@ -57,6 +57,7 @@ export function createSkillWorkflow( description: opts.description, flowKey: opts.flowKey, steps: AGENT_SKILL_STEPS, + reportFile: opts.reportFile, run: { skillId: opts.skillId, integrationLabel: opts.integrationLabel, diff --git a/src/lib/workflows/audit-3000/index.ts b/src/lib/workflows/audit-3000/index.ts new file mode 100644 index 00000000..abe3438b --- /dev/null +++ b/src/lib/workflows/audit-3000/index.ts @@ -0,0 +1,127 @@ +import fs from 'fs'; +import path from 'path'; +import { + AGENT_SKILL_STEPS, + createSkillWorkflow, +} from '../agent-skill/index.js'; +import type { Workflow, WorkflowConfig } from '../workflow-step.js'; +import type { WorkflowRun } from '../../agent/agent-runner.js'; +import type { WizardSession } from '../../wizard-session.js'; +import { AUDIT_ABORT_CASES } from '../audit/detect.js'; +import { + AUDIT_CHECKS_FILE, + AUDIT_CHECKS_KEY, + type AuditCheck, +} from '../audit/types.js'; +import { AUDIT_SEED_CHECKS } from '../audit/seed.js'; +import { logToFile } from '../../../utils/debug'; + +const AUDIT3000_REPORT_FILE = 'posthog-audit-3000-report.md'; + +// Five extra checks the v3000 audit adds on top of the base 10. +// IDs must match those referenced in the audit-3000 skill's step files. +const AUDIT3000_EXTRA_CHECKS: AuditCheck[] = [ + { + id: 'event-naming-standardization', + area: 'Event Quality', + label: 'Event naming convention is consistent', + status: 'pending', + }, + { + id: 'event-duplicates-and-bloat', + area: 'Event Quality', + label: 'No duplicate or bloated event capture', + status: 'pending', + }, + { + id: 'event-quality-context-review', + area: 'Event Quality', + label: 'Event property context reviewed', + status: 'pending', + }, + { + id: 'event-usage-coverage', + area: 'Event Quality', + label: 'Captured events match insights / dashboards usage', + status: 'pending', + }, + { + id: 'stale-feature-flags-reviewed', + area: 'Feature Flags', + label: 'Stale feature flags reviewed', + status: 'pending', + }, +]; + +const AUDIT3000_SEED_CHECKS: AuditCheck[] = [ + ...AUDIT_SEED_CHECKS, + ...AUDIT3000_EXTRA_CHECKS, +]; + +// Intro is custom; run/outro reuse the audit screens. Those screens read +// the report path from WorkflowConfig.reportFile, so the v3000 label is +// rendered correctly without forking the screens. +const AUDIT3000_SCREEN_BY_STEP: Record = { + intro: 'audit-3000-intro', + run: 'audit-run', + outro: 'audit-outro', +}; + +const seedAudit3000Ledger = (installDir: string): void => { + const target = path.join(installDir, AUDIT_CHECKS_FILE); + const tmp = `${target}.tmp`; + fs.writeFileSync(tmp, JSON.stringify(AUDIT3000_SEED_CHECKS, null, 2), 'utf8'); + fs.renameSync(tmp, target); + logToFile( + `seedAudit3000Ledger: wrote ${AUDIT3000_SEED_CHECKS.length} entries to ${target}`, + ); +}; + +const seedBeforeAudit3000Run = (session: WizardSession): void => { + seedAudit3000Ledger(session.installDir); + session.frameworkContext[AUDIT_CHECKS_KEY] = AUDIT3000_SEED_CHECKS; +}; + +const withAudit3000Screens = (steps: Workflow): Workflow => + steps.map((step) => { + const override = AUDIT3000_SCREEN_BY_STEP[step.id]; + return override ? { ...step, screen: override } : step; + }); + +const audit3000Steps: Workflow = withAudit3000Screens(AGENT_SKILL_STEPS); + +const baseConfig = createSkillWorkflow({ + skillId: 'audit-3000', + command: 'audit-3000', + flowKey: 'audit-3000', + description: + 'Audit an existing PostHog integration (v3000 — adds event quality, stale-flag hygiene, customer enrichment, use-case match)', + integrationLabel: 'audit-3000', + customPrompt: + 'Run the audit-3000 skill end-to-end. Follow the step chain starting at references/1-version.md. Do not modify any project files — only create the final audit report and (when enrichment is enabled) the enrichment report.', + successMessage: `Audit complete! View the report at ./${AUDIT3000_REPORT_FILE}`, + reportFile: AUDIT3000_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/product-analytics/best-practices', + spinnerMessage: 'Running PostHog Audit 3000...', + estimatedDurationMinutes: 6, + requires: ['posthog-integration'], + abortCases: AUDIT_ABORT_CASES, +}); + +const audit3000Run = async (session: WizardSession): Promise => { + seedBeforeAudit3000Run(session); + + if (!baseConfig.run) { + throw new Error('audit-3000 workflow has no run configuration.'); + } + + return typeof baseConfig.run === 'function' + ? baseConfig.run(session) + : baseConfig.run; +}; + +export const audit3000Config: WorkflowConfig = { + ...baseConfig, + steps: audit3000Steps, + run: audit3000Run, +}; diff --git a/src/lib/workflows/workflow-registry.ts b/src/lib/workflows/workflow-registry.ts index 3a729dad..b4521092 100644 --- a/src/lib/workflows/workflow-registry.ts +++ b/src/lib/workflows/workflow-registry.ts @@ -15,12 +15,14 @@ import type { WorkflowConfig } from './workflow-step.js'; import { posthogIntegrationConfig } from './posthog-integration/index.js'; import { revenueAnalyticsConfig } from './revenue-analytics/index.js'; import { auditConfig } from './audit/index.js'; +import { audit3000Config } from './audit-3000/index.js'; import { posthogDoctorConfig } from './posthog-doctor/index.js'; export const WORKFLOW_REGISTRY: WorkflowConfig[] = [ posthogIntegrationConfig, revenueAnalyticsConfig, auditConfig, + audit3000Config, posthogDoctorConfig, ]; diff --git a/src/lib/workflows/workflow-step.ts b/src/lib/workflows/workflow-step.ts index cc7bd0f6..03d302da 100644 --- a/src/lib/workflows/workflow-step.ts +++ b/src/lib/workflows/workflow-step.ts @@ -123,6 +123,12 @@ export interface WorkflowConfig { run?: WorkflowRun | ((session: WizardSession) => Promise); /** Prerequisites: other workflow flowKeys that must have run first */ requires?: string[]; + /** + * Path (relative to installDir) of the report file the workflow writes. + * Mirrors `run.reportFile` but lifted to the top level so UI screens can + * read it synchronously without resolving a deferred `run` function. + */ + reportFile?: string; } /** diff --git a/src/ui/tui/flows.ts b/src/ui/tui/flows.ts index 79a9e0ab..6eb8f2e1 100644 --- a/src/ui/tui/flows.ts +++ b/src/ui/tui/flows.ts @@ -27,6 +27,7 @@ export enum Screen { AuditIntro = 'audit-intro', AuditRun = 'audit-run', AuditOutro = 'audit-outro', + Audit3000Intro = 'audit-3000-intro', HealthCheck = 'health-check', DoctorIntro = 'doctor-intro', DoctorReport = 'doctor-report', @@ -46,6 +47,7 @@ export enum Flow { PostHogIntegration = 'posthog-integration', RevenueAnalyticsSetup = 'revenue-analytics-setup', Audit = 'audit', + Audit3000 = 'audit-3000', PosthogDoctor = 'posthog-doctor', AgentSkill = 'agent-skill', McpAdd = 'mcp-add', diff --git a/src/ui/tui/screen-registry.tsx b/src/ui/tui/screen-registry.tsx index 08128854..03584163 100644 --- a/src/ui/tui/screen-registry.tsx +++ b/src/ui/tui/screen-registry.tsx @@ -24,6 +24,7 @@ import { AgentSkillIntroScreen } from './screens/AgentSkillIntroScreen.js'; import { AuditIntroScreen } from './screens/audit/AuditIntroScreen.js'; import { AuditRunScreen } from './screens/audit/AuditRunScreen.js'; import { AuditOutroScreen } from './screens/audit/AuditOutroScreen.js'; +import { Audit3000IntroScreen } from './screens/audit-3000/Audit3000IntroScreen.js'; import { SetupScreen } from './screens/SetupScreen.js'; import { AuthScreen } from './screens/AuthScreen.js'; import { RunScreen } from './screens/RunScreen.js'; @@ -63,6 +64,7 @@ export function createScreens( [Screen.AuditIntro]: , [Screen.AuditRun]: , [Screen.AuditOutro]: , + [Screen.Audit3000Intro]: , [Screen.HealthCheck]: , [Screen.DoctorIntro]: , [Screen.DoctorReport]: , diff --git a/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx b/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx new file mode 100644 index 00000000..2139032b --- /dev/null +++ b/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx @@ -0,0 +1,89 @@ +import { Box, Text } from 'ink'; +import { useState, useSyncExternalStore } from 'react'; +import type { WizardStore } from '../../store.js'; +import { IntroScreenLayout } from '../IntroScreenLayout.js'; +import { SkillSourceInfo, useSkillEntry } from '../SkillSourceInfo.js'; + +const AUDIT3000_SKILL_ID = 'audit-3000'; + +interface Audit3000IntroScreenProps { + store: WizardStore; +} + +export const Audit3000IntroScreen = ({ store }: Audit3000IntroScreenProps) => { + useSyncExternalStore( + (cb) => store.subscribe(cb), + () => store.getSnapshot(), + ); + + const [showingMoreInfo, setShowingMoreInfo] = useState(false); + const { session } = store; + const { skillEntry, fetchFailed } = useSkillEntry( + AUDIT3000_SKILL_ID, + 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{' '} + + {AUDIT3000_SKILL_ID} + {' '} + workflow reviews your PostHog integration against best practices — SDK + install, identification, event capture, event quality, and stale + feature-flag hygiene — and writes a report with suggested actions. When + enrichment is available, it also produces a separate company profile + + use-case match. Nothing in your project is modified. + + + + + + ) : ( + + + Let's run a deep review of your PostHog setup and surface concrete next + steps. + + + ); + + 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 ( + + ); +}; diff --git a/src/ui/tui/screens/audit/AuditOutroScreen.tsx b/src/ui/tui/screens/audit/AuditOutroScreen.tsx index 1275e30d..09f4e60a 100644 --- a/src/ui/tui/screens/audit/AuditOutroScreen.tsx +++ b/src/ui/tui/screens/audit/AuditOutroScreen.tsx @@ -1,9 +1,11 @@ /** * AuditOutroScreen — Audit-specific post-run summary. Renders the standard * success / error / cancel views with the audit checks summary inlined into - * the success body. The report path is hardcoded to AUDIT_REPORT_FILE. + * the success body. The report path shown in the success headline comes from + * the workflow's `successMessage`, so this screen is workflow-agnostic. */ +import { join } from 'node:path'; import { Box, Text, useInput } from 'ink'; import { useSyncExternalStore } from 'react'; import type { WizardStore } from '../../store.js'; @@ -44,6 +46,21 @@ export const AuditOutroScreen = ({ store }: AuditOutroScreenProps) => { ✔ {outroData.message || 'Audit complete!'} + {outroData.reportFile && ( + + + Report saved to: + + + {join(store.session.installDir, outroData.reportFile)} + + + A markdown file in your project folder — open it in any editor + to read the full audit. + + + )} + { const [columns] = useStdoutDimensions(); const checks = getAuditChecks(store.session); - const reportPath = `./${AUDIT_REPORT_FILE}`; + const reportFile = + getWorkflowConfig(store.router.activeFlow)?.reportFile ?? AUDIT_REPORT_FILE; + const reportPath = `./${reportFile}`; const pendingChecksList = ; const areaPane = ; From 6be150e5bb54547437b19ccb678694b900200523 Mon Sep 17 00:00:00 2001 From: Jon <148268521+consultingsultan@users.noreply.github.com> Date: Thu, 14 May 2026 11:04:47 -0400 Subject: [PATCH 2/8] feat(audit-3000): arcade-flavoured intro / run / outro screens Adds dedicated audit-3000 TUI surfaces and fills out the seed ledger to match every check the audit-3000 skill writes: - Intro: ASCII "AUDIT-3000" banner, blinking INSERT COIN tagline, PRESS START menu, 23-check tagline. - Run: arcade SCORE panel (SCORE n/23, PASS / MISS / QUEUE counters, LEVEL n prefixes per area), six area slides including new Event Quality, Stale Flags, and Use Case: Expansion levels. - Outro: GAME OVER / FINAL SCORE high-score tile with pass/miss tally, then absolute report path and the standard findings list. - Seed: adds the 8 missing expansion-* entries so Use Case: Expansion renders alongside the existing Installation / Identification / Event Capture / Event Quality / Feature Flags areas (23 total). - Wires Flow.Audit3000{Run,Outro} and the matching Screen enum / screen registry entries. Generated-By: PostHog Code Task-Id: 598dae15-1b52-45e2-8c14-e59aafb1ab9b --- src/lib/workflows/audit-3000/index.ts | 62 ++++- src/ui/tui/flows.ts | 2 + src/ui/tui/screen-registry.tsx | 4 + .../screens/audit-3000/Audit3000AreaPane.tsx | 142 ++++++++++++ .../audit-3000/Audit3000ChecksPanel.tsx | 200 ++++++++++++++++ .../audit-3000/Audit3000IntroScreen.tsx | 82 ++++++- .../audit-3000/Audit3000OutroScreen.tsx | 213 ++++++++++++++++++ .../screens/audit-3000/Audit3000RunScreen.tsx | 91 ++++++++ .../audit-3000/slides/eventQuality.tsx | 34 +++ .../screens/audit-3000/slides/expansion.tsx | 37 +++ .../audit-3000/slides/featureFlags.tsx | 33 +++ src/ui/tui/screens/audit-3000/slides/index.ts | 24 ++ 12 files changed, 906 insertions(+), 18 deletions(-) create mode 100644 src/ui/tui/screens/audit-3000/Audit3000AreaPane.tsx create mode 100644 src/ui/tui/screens/audit-3000/Audit3000ChecksPanel.tsx create mode 100644 src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx create mode 100644 src/ui/tui/screens/audit-3000/Audit3000RunScreen.tsx create mode 100644 src/ui/tui/screens/audit-3000/slides/eventQuality.tsx create mode 100644 src/ui/tui/screens/audit-3000/slides/expansion.tsx create mode 100644 src/ui/tui/screens/audit-3000/slides/featureFlags.tsx create mode 100644 src/ui/tui/screens/audit-3000/slides/index.ts diff --git a/src/lib/workflows/audit-3000/index.ts b/src/lib/workflows/audit-3000/index.ts index abe3438b..d8ac6b90 100644 --- a/src/lib/workflows/audit-3000/index.ts +++ b/src/lib/workflows/audit-3000/index.ts @@ -18,8 +18,9 @@ import { logToFile } from '../../../utils/debug'; const AUDIT3000_REPORT_FILE = 'posthog-audit-3000-report.md'; -// Five extra checks the v3000 audit adds on top of the base 10. -// IDs must match those referenced in the audit-3000 skill's step files. +// Extra checks the v3000 audit adds on top of the base 10. IDs must match +// those referenced in the audit-3000 skill's step files (Event Quality, +// stale feature-flag review, and per-product use-case expansion). const AUDIT3000_EXTRA_CHECKS: AuditCheck[] = [ { id: 'event-naming-standardization', @@ -51,6 +52,54 @@ const AUDIT3000_EXTRA_CHECKS: AuditCheck[] = [ label: 'Stale feature flags reviewed', status: 'pending', }, + { + id: 'expansion-product-analytics', + area: 'Use Case: Expansion', + label: 'Product analytics coverage', + status: 'pending', + }, + { + id: 'expansion-error-tracking', + area: 'Use Case: Expansion', + label: 'Error tracking coverage', + status: 'pending', + }, + { + id: 'expansion-llm-observability', + area: 'Use Case: Expansion', + label: 'LLM observability coverage', + status: 'pending', + }, + { + id: 'expansion-session-replay', + area: 'Use Case: Expansion', + label: 'Session replay coverage', + status: 'pending', + }, + { + id: 'expansion-feature-flags', + area: 'Use Case: Expansion', + label: 'Feature flags coverage', + status: 'pending', + }, + { + id: 'expansion-surveys', + area: 'Use Case: Expansion', + label: 'Surveys coverage', + status: 'pending', + }, + { + id: 'expansion-logs', + area: 'Use Case: Expansion', + label: 'Logs coverage', + status: 'pending', + }, + { + id: 'expansion-web-analytics', + area: 'Use Case: Expansion', + label: 'Web analytics coverage', + status: 'pending', + }, ]; const AUDIT3000_SEED_CHECKS: AuditCheck[] = [ @@ -58,13 +107,12 @@ const AUDIT3000_SEED_CHECKS: AuditCheck[] = [ ...AUDIT3000_EXTRA_CHECKS, ]; -// Intro is custom; run/outro reuse the audit screens. Those screens read -// the report path from WorkflowConfig.reportFile, so the v3000 label is -// rendered correctly without forking the screens. +// Audit-3000 has its own arcade-flavoured intro / run / outro screens. The +// shared audit screens stay reserved for the original `audit` workflow. const AUDIT3000_SCREEN_BY_STEP: Record = { intro: 'audit-3000-intro', - run: 'audit-run', - outro: 'audit-outro', + run: 'audit-3000-run', + outro: 'audit-3000-outro', }; const seedAudit3000Ledger = (installDir: string): void => { diff --git a/src/ui/tui/flows.ts b/src/ui/tui/flows.ts index 6eb8f2e1..b83446aa 100644 --- a/src/ui/tui/flows.ts +++ b/src/ui/tui/flows.ts @@ -28,6 +28,8 @@ export enum Screen { AuditRun = 'audit-run', AuditOutro = 'audit-outro', Audit3000Intro = 'audit-3000-intro', + Audit3000Run = 'audit-3000-run', + Audit3000Outro = 'audit-3000-outro', HealthCheck = 'health-check', DoctorIntro = 'doctor-intro', DoctorReport = 'doctor-report', diff --git a/src/ui/tui/screen-registry.tsx b/src/ui/tui/screen-registry.tsx index 03584163..819e70f3 100644 --- a/src/ui/tui/screen-registry.tsx +++ b/src/ui/tui/screen-registry.tsx @@ -25,6 +25,8 @@ import { AuditIntroScreen } from './screens/audit/AuditIntroScreen.js'; import { AuditRunScreen } from './screens/audit/AuditRunScreen.js'; import { AuditOutroScreen } from './screens/audit/AuditOutroScreen.js'; import { Audit3000IntroScreen } from './screens/audit-3000/Audit3000IntroScreen.js'; +import { Audit3000RunScreen } from './screens/audit-3000/Audit3000RunScreen.js'; +import { Audit3000OutroScreen } from './screens/audit-3000/Audit3000OutroScreen.js'; import { SetupScreen } from './screens/SetupScreen.js'; import { AuthScreen } from './screens/AuthScreen.js'; import { RunScreen } from './screens/RunScreen.js'; @@ -65,6 +67,8 @@ export function createScreens( [Screen.AuditRun]: , [Screen.AuditOutro]: , [Screen.Audit3000Intro]: , + [Screen.Audit3000Run]: , + [Screen.Audit3000Outro]: , [Screen.HealthCheck]: , [Screen.DoctorIntro]: , [Screen.DoctorReport]: , diff --git a/src/ui/tui/screens/audit-3000/Audit3000AreaPane.tsx b/src/ui/tui/screens/audit-3000/Audit3000AreaPane.tsx new file mode 100644 index 00000000..fd4c4b8b --- /dev/null +++ b/src/ui/tui/screens/audit-3000/Audit3000AreaPane.tsx @@ -0,0 +1,142 @@ +/** + * Audit-3000 right pane — arcade-flavoured fork of `AuditAreaPane`. + * + * Mirrors the audit pane's three-state logic (active slide → empty → + * wrap-up) but routes through the audit-3000 slide registry and uses + * "LEVEL N: " framing instead of "Verifying ...". + */ + +import { Fragment } from 'react'; +import { Box, Text, useInput } from 'ink'; +import { spawn } from 'node:child_process'; +import { Colors } from '../../styles.js'; +import { type AuditCheck } from '../../../../lib/workflows/audit/types.js'; +import { AUDIT_3000_AREA_SLIDES, type AreaSlide } from './slides/index.js'; + +const FINDING_STATUSES: AuditCheck['status'][] = [ + 'error', + 'warning', + 'suggestion', +]; + +const isFinding = (c: AuditCheck) => FINDING_STATUSES.includes(c.status); + +const fallbackSlide = (area: string): AreaSlide => ({ + area, + intro: [`Now playing: ${area.toLowerCase()}\u2026`], + docsUrl: '', +}); + +const openLink = (url: string) => { + const cmd = + process.platform === 'darwin' + ? 'open' + : process.platform === 'win32' + ? 'cmd' + : 'xdg-open'; + const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]; + spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref(); +}; + +interface Audit3000AreaPaneProps { + checks: AuditCheck[]; + reportPath: string; +} + +export const Audit3000AreaPane = ({ + checks, + reportPath, +}: Audit3000AreaPaneProps) => { + const pendingChecks = checks.filter((c) => c.status === 'pending'); + const activeArea = pendingChecks[0]?.area; + const slide = activeArea + ? AUDIT_3000_AREA_SLIDES.find((s) => s.area === activeArea) ?? + fallbackSlide(activeArea) + : null; + + const levelIndex = activeArea + ? AUDIT_3000_AREA_SLIDES.findIndex((s) => s.area === activeArea) + : -1; + const level = levelIndex >= 0 ? levelIndex + 1 : null; + + useInput((input) => { + if (input.toLowerCase() === 'o' && slide?.docsUrl) { + openLink(slide.docsUrl); + } + }); + + if (slide) { + const hasFindings = checks.some(isFinding); + return ( + + ); + } + + if (checks.length === 0) { + return null; + } + + return ; +}; + +const ActiveSlide = ({ + slide, + level, + hasFindings, +}: { + slide: AreaSlide; + level: number | null; + hasFindings: boolean; +}) => ( + + + {level ? `LEVEL ${level}: ` : ''} + {slide.area.toUpperCase()} + + + + {slide.visual} + {slide.intro.map((paragraph, i) => ( + + {i > 0 && } + {paragraph} + + ))} + + + + {slide.docsUrl && ( + <> + [O] Learn more + + )} + {hasFindings && ( + <> + {slide.docsUrl && ' '}[ + {'\u2192'}] View issues + + )} + + + +); + +const WritingReport = ({ reportPath }: { reportPath: string }) => ( + + + STAGE CLEAR. + + + + All checks resolved. Compiling your high-score reel at{' '} + {reportPath}. + + + + The report covers everything we checked, what we found, and what to do + next. + + + {'Stand by\u2026'} + +); diff --git a/src/ui/tui/screens/audit-3000/Audit3000ChecksPanel.tsx b/src/ui/tui/screens/audit-3000/Audit3000ChecksPanel.tsx new file mode 100644 index 00000000..b1ae843a --- /dev/null +++ b/src/ui/tui/screens/audit-3000/Audit3000ChecksPanel.tsx @@ -0,0 +1,200 @@ +/** + * Audit-3000 left pane on the Run screen. Arcade-flavoured fork of the + * audit workflow's `PendingChecksList`: a running score banner sits on + * top, then the standard grouped check ledger underneath. + * + * The grouped check rendering is intentionally near-identical to the + * original so wide and narrow terminals behave the same; the arcade + * dressing lives entirely in the `ScoreBanner` and group level labels. + */ + +import { Box, Text } from 'ink'; +import { Spinner } from '@inkjs/ui'; +import { + AUDIT_SEVERITY_STYLE, + type AuditCheck, + type AuditStatus, +} from '../../../../lib/workflows/audit/types.js'; +import { Colors, Icons } from '../../styles.js'; +import { LoadingBox } from '../../primitives/index.js'; +import { useStdoutDimensions } from '../../hooks/useStdoutDimensions.js'; + +const COLLAPSE_BELOW_ROWS = 24; + +const NEON_PINK = '#F54E00'; +const NEON_GOLD = '#F9BD2B'; +const NEON_BLUE = '#1D4AFF'; + +interface Audit3000ChecksPanelProps { + checks: AuditCheck[]; +} + +interface Group { + area: string; + checks: AuditCheck[]; +} + +function groupByArea(checks: AuditCheck[]): Group[] { + const order: string[] = []; + const map = new Map(); + for (const c of checks) { + if (!map.has(c.area)) { + map.set(c.area, []); + order.push(c.area); + } + map.get(c.area)!.push(c); + } + return order.map((area) => ({ area, checks: map.get(area)! })); +} + +function countByStatus(checks: AuditCheck[]): Record { + const counts: Record = { + pending: 0, + pass: 0, + error: 0, + warning: 0, + suggestion: 0, + }; + for (const c of checks) counts[c.status] += 1; + return counts; +} + +const ScoreBanner = ({ checks }: { checks: AuditCheck[] }) => { + const counts = countByStatus(checks); + const resolved = checks.length - counts.pending; + const issues = counts.error + counts.warning + counts.suggestion; + + return ( + + + + {'SCORE '} + + + {resolved.toString().padStart(2, '0')} + + {' / '} + {checks.length.toString().padStart(2, '0')} + + + {`PASS \u25B2 ${counts.pass}`} + {' '} + {`MISS \u25BC ${issues}`} + {' '} + {`QUEUE \u25CB ${counts.pending}`} + + + ); +}; + +function groupIcon(group: Group): { icon: string; color: string } { + const total = group.checks.length; + const complete = group.checks.filter((c) => c.status !== 'pending').length; + if (complete === 0) return { icon: Icons.squareOpen, color: Colors.muted }; + if (complete === total) + return { icon: Icons.squareFilled, color: Colors.success }; + return { icon: Icons.triangleRight, color: Colors.primary }; +} + +const GroupHeader = ({ + group, + level, + showIcon, + isActive, +}: { + group: Group; + level: number; + showIcon: boolean; + isActive: boolean; +}) => { + const complete = group.checks.filter((c) => c.status !== 'pending').length; + const total = group.checks.length; + const { icon, color } = groupIcon(group); + return ( + + {isActive ? ( + + + + ) : showIcon ? ( + + {icon}{' '} + + ) : null} + + {`L${level} `} + {group.area}{' '} + + ({complete}/{total}) + + + + ); +}; + +const CheckRow = ({ check }: { check: AuditCheck }) => { + const { glyph, color } = AUDIT_SEVERITY_STYLE[check.status]; + return ( + + {glyph} + {check.label} + + ); +}; + +export const Audit3000ChecksPanel = ({ checks }: Audit3000ChecksPanelProps) => { + const [, termRows] = useStdoutDimensions(); + + if (checks.length === 0) { + return ( + + AUDIT-3000 + + + + ); + } + + const groups = groupByArea(checks); + const activeIndex = groups.findIndex((g) => + g.checks.some((c) => c.status === 'pending'), + ); + const collapsed = termRows < COLLAPSE_BELOW_ROWS; + + return ( + + + AUDIT-3000 + + + + {collapsed + ? groups.map((group, i) => ( + + )) + : groups.map((group, i) => ( + + + {group.checks.map((c) => ( + + ))} + + ))} + + ); +}; diff --git a/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx b/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx index 2139032b..de27e906 100644 --- a/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx +++ b/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx @@ -1,11 +1,68 @@ import { Box, Text } from 'ink'; -import { useState, useSyncExternalStore } from 'react'; +import { useEffect, useState, useSyncExternalStore } from 'react'; import type { WizardStore } from '../../store.js'; import { IntroScreenLayout } from '../IntroScreenLayout.js'; import { SkillSourceInfo, useSkillEntry } from '../SkillSourceInfo.js'; const AUDIT3000_SKILL_ID = 'audit-3000'; +// PostHog brand palette, tuned for the arcade panel. +const NEON_PINK = '#F54E00'; +const NEON_BLUE = '#1D4AFF'; +const NEON_GOLD = '#F9BD2B'; + +const ArcadeBanner = () => { + // Blink the "INSERT COIN" tagline once per 600ms — classic attract-mode + // pacing without burning Ink with rapid re-renders. + const [blinkOn, setBlinkOn] = useState(true); + useEffect(() => { + const id = setInterval(() => setBlinkOn((v) => !v), 600); + return () => clearInterval(id); + }, []); + + const top = '\u250F' + '\u2501'.repeat(32) + '\u2513'; + const bottom = '\u2517' + '\u2501'.repeat(32) + '\u251B'; + + return ( + + + {top} + + + + {'\u2503'} + + + {' A U D I T '} + + + {'-'} + + + {' 3 0 0 0 '} + + + {'\u2503'} + + + + + {'\u2503'} + + + {' \u25B6 INSERT COIN TO PLAY \u25C0 '} + + + {'\u2503'} + + + + {bottom} + + + ); +}; + interface Audit3000IntroScreenProps { store: WizardStore; } @@ -37,11 +94,11 @@ export const Audit3000IntroScreen = ({ store }: Audit3000IntroScreenProps) => { {AUDIT3000_SKILL_ID} {' '} - workflow reviews your PostHog integration against best practices — SDK - install, identification, event capture, event quality, and stale - feature-flag hygiene — and writes a report with suggested actions. When - enrichment is available, it also produces a separate company profile + - use-case match. Nothing in your project is modified. + workflow reviews your PostHog integration across 23 checks — SDK + install, identification, event capture, event quality, stale feature + flag hygiene, and use-case expansion across 8 PostHog products. When + enrichment is available it also produces a company profile and use-case + match. Nothing in your project is modified. { ) : ( - - Let's run a deep review of your PostHog setup and surface concrete next - steps. - + + + 23 checks. 6 levels. 1 final report. + + High-score your PostHog integration before the boss fight. + + ); const menuOptions = showingMoreInfo ? [{ label: 'Back', value: 'back' }] : [ - { label: 'Continue', value: 'continue' }, + { label: 'PRESS START', value: 'continue' }, { label: 'More info', value: 'more-info' }, { label: 'Cancel', value: 'cancel' }, ]; diff --git a/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx b/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx new file mode 100644 index 00000000..8c965380 --- /dev/null +++ b/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx @@ -0,0 +1,213 @@ +/** + * Audit3000OutroScreen — high-score-style summary after a v3000 audit run. + * + * On success: arcade FINAL SCORE banner with pass / miss tallies, the + * absolute report path, and the standard problematic-items list. + * + * Error and cancel branches mirror `AuditOutroScreen` so failure modes + * stay legible without arcade dressing. + */ + +import { join } from 'node:path'; +import { Box, Text, useInput } from 'ink'; +import { useSyncExternalStore } from 'react'; +import type { WizardStore } from '../../store.js'; +import { OutroKind } from '../../../../lib/wizard-session.js'; +import { Colors } from '../../styles.js'; +import { + getAuditChecks, + type AuditCheck, + type AuditStatus, +} from '../../../../lib/workflows/audit/types.js'; +import { AuditChecksOutroSection } from '../audit/AuditChecksOutroSection.js'; + +const NEON_PINK = '#F54E00'; +const NEON_GOLD = '#F9BD2B'; +const NEON_BLUE = '#1D4AFF'; + +const PANEL_WIDTH = 48; + +const padCenter = (s: string, width: number): string => { + if (s.length >= width) return s; + const total = width - s.length; + const left = Math.floor(total / 2); + const right = total - left; + return ' '.repeat(left) + s + ' '.repeat(right); +}; + +function countByStatus(checks: AuditCheck[]): Record { + const counts: Record = { + pending: 0, + pass: 0, + error: 0, + warning: 0, + suggestion: 0, + }; + for (const c of checks) counts[c.status] += 1; + return counts; +} + +const FinalScorePanel = ({ checks }: { checks: AuditCheck[] }) => { + const counts = countByStatus(checks); + const resolved = checks.length - counts.pending; + const issues = counts.error + counts.warning + counts.suggestion; + + const top = '\u250F' + '\u2501'.repeat(PANEL_WIDTH) + '\u2513'; + const bottom = '\u2517' + '\u2501'.repeat(PANEL_WIDTH) + '\u251B'; + const sep = '\u2520' + '\u2500'.repeat(PANEL_WIDTH) + '\u2528'; + + const row = (content: string) => ( + + + {'\u2503'} + + {content} + + {'\u2503'} + + + ); + + return ( + + + {top} + + {row(padCenter('GAME OVER', PANEL_WIDTH))} + + + {'\u2503'} + + + {padCenter( + `FINAL SCORE ${resolved} / ${checks.length}`, + PANEL_WIDTH, + )} + + + {'\u2503'} + + + {sep} + + + {'\u2503'} + + + {padCenter(`PASS \u25B2 ${counts.pass}`, PANEL_WIDTH)} + + + {'\u2503'} + + + + + {'\u2503'} + + + {padCenter(`MISS \u25BC ${issues}`, PANEL_WIDTH)} + + + {'\u2503'} + + + + {bottom} + + + ); +}; + +interface Audit3000OutroScreenProps { + store: WizardStore; +} + +export const Audit3000OutroScreen = ({ store }: Audit3000OutroScreenProps) => { + useSyncExternalStore( + (cb) => store.subscribe(cb), + () => store.getSnapshot(), + ); + + useInput(() => { + store.setOutroDismissed(); + }); + + const outroData = store.session.outroData; + + if (!outroData) { + return ( + + Counting your tokens\u2026 + + ); + } + + const checks = getAuditChecks(store.session); + + return ( + + {outroData.kind === OutroKind.Success && ( + + + + + + {'\u2714'} {outroData.message || 'AUDIT-3000 complete!'} + + + + {outroData.reportFile && ( + + + High-score reel saved to: + + + {join(store.session.installDir, outroData.reportFile)} + + + A markdown file in your project folder — open it in any editor + to read the full audit. + + + )} + + + + {outroData.docsUrl && ( + + + Learn more: {outroData.docsUrl} + + + )} + + )} + + {outroData.kind === OutroKind.Error && ( + + + {'\u2718'} {outroData.message || 'An error occurred'} + + {outroData.body && ( + + {outroData.body} + + )} + + )} + + {outroData.kind === OutroKind.Cancel && ( + + {'\u25A0'} {outroData.message || 'Cancelled'} + + )} + + + Press any key to continue + + + ); +}; diff --git a/src/ui/tui/screens/audit-3000/Audit3000RunScreen.tsx b/src/ui/tui/screens/audit-3000/Audit3000RunScreen.tsx new file mode 100644 index 00000000..f6a05396 --- /dev/null +++ b/src/ui/tui/screens/audit-3000/Audit3000RunScreen.tsx @@ -0,0 +1,91 @@ +import { useSyncExternalStore } from 'react'; +import { join } from 'node:path'; +import { Box } from 'ink'; +import type { WizardStore } from '../../store.js'; +import { + TabContainer, + SplitView, + LogViewer, + HNViewer, +} from '../../primitives/index.js'; +import { useStdoutDimensions } from '../../hooks/useStdoutDimensions.js'; +import { useFileWatcher } from '../../hooks/file-watcher.js'; +import { AuditChecksViewer } from '../audit/AuditChecksViewer/AuditChecksViewer.js'; +import { Audit3000AreaPane } from './Audit3000AreaPane.js'; +import { Audit3000ChecksPanel } from './Audit3000ChecksPanel.js'; +import { + AUDIT_CHECKS_FILE, + AUDIT_CHECKS_KEY, + coerceAuditChecks, + getAuditChecks, +} from '../../../../lib/workflows/audit/types.js'; +import { getWorkflowConfig } from '../../../../lib/workflows/workflow-registry.js'; +import { WIZARD_LOG_FILE } from '../../../../utils/paths.js'; + +const AUDIT_3000_REPORT_FILE_FALLBACK = 'posthog-audit-3000-report.md'; + +interface Audit3000RunScreenProps { + store: WizardStore; +} + +export const Audit3000RunScreen = ({ store }: Audit3000RunScreenProps) => { + useSyncExternalStore( + (cb) => store.subscribe(cb), + () => store.getSnapshot(), + ); + + // Mirror the agent's audit ledger into the store. The audit-3000 skill + // writes to the same `.posthog-audit-checks.json` path the original + // audit uses, so the file watcher key is shared. + useFileWatcher(join(store.session.installDir, AUDIT_CHECKS_FILE), (parsed) => + store.setFrameworkContext(AUDIT_CHECKS_KEY, coerceAuditChecks(parsed)), + ); + + const statuses = + store.statusMessages.length > 0 ? store.statusMessages : undefined; + + const [columns] = useStdoutDimensions(); + const checks = getAuditChecks(store.session); + const reportFile = + getWorkflowConfig(store.router.activeFlow)?.reportFile ?? + AUDIT_3000_REPORT_FILE_FALLBACK; + const reportPath = `./${reportFile}`; + const checksPanel = ; + const areaPane = ( + + ); + + // Narrow terminals: drop the area pane. + const statusComponent = + columns < 80 ? ( + + {checksPanel} + + ) : ( + + ); + + const tabs = [ + { id: 'status', label: 'Arcade', component: statusComponent }, + { + id: 'audit-checks', + label: 'Hi-score table', + component: , + }, + { + id: 'logs', + label: 'Tail logs', + component: , + }, + { id: 'hn', label: 'HN', component: }, + ]; + + return ( + + ); +}; diff --git a/src/ui/tui/screens/audit-3000/slides/eventQuality.tsx b/src/ui/tui/screens/audit-3000/slides/eventQuality.tsx new file mode 100644 index 00000000..e028ad78 --- /dev/null +++ b/src/ui/tui/screens/audit-3000/slides/eventQuality.tsx @@ -0,0 +1,34 @@ +import { Text } from 'ink'; +import { VisualBox, type AreaSlide } from '../../audit/slides/shared.js'; + +const EventQualityVisual = () => ( + + + {'event_clicked '} + {'\u2713'} + + + {'eventClicked '} + {'~ duplicate?'} + + + {'click_event '} + {'~ duplicate?'} + + + {'big_kitchen_sink '} + {'\u2717 22 props'} + + +); + +export const EventQualitySlide: AreaSlide = { + area: 'Event Quality', + intro: [ + 'LEVEL 5: EVENT QUALITY. The capture call-sites are clean. The events themselves are the real boss fight.', + 'Scanning for: naming inconsistencies, semantic duplicates, kitchen-sink event payloads, and (if your PostHog project is linked) which captured events actually drive insights and dashboards.', + '4 subagents fan out in parallel. The ticker shows them clearing checks live.', + ], + visual: , + docsUrl: 'https://posthog.com/docs/product-analytics/best-practices', +}; diff --git a/src/ui/tui/screens/audit-3000/slides/expansion.tsx b/src/ui/tui/screens/audit-3000/slides/expansion.tsx new file mode 100644 index 00000000..8a9ab8e4 --- /dev/null +++ b/src/ui/tui/screens/audit-3000/slides/expansion.tsx @@ -0,0 +1,37 @@ +import { Text } from 'ink'; +import { VisualBox, type AreaSlide } from '../../audit/slides/shared.js'; + +const ExpansionVisual = () => ( + + + {'product analytics '} + {'\u25A0\u25A0\u25A0\u25A0\u25A0'} + + + {'error tracking '} + {'\u25A1\u25A1\u25A1\u25A1\u25A1'} + {' sentry detected'} + + + {'session replay '} + {'\u25A0\u25A0\u25A1\u25A1\u25A1'} + {' partial'} + + + {'llm observability '} + {'\u25A1\u25A1\u25A1\u25A1\u25A1'} + {' greenfield'} + + +); + +export const ExpansionSlide: AreaSlide = { + area: 'Use Case: Expansion', + intro: [ + 'BONUS ROUND: EXPANSION. You might be paying for tools PostHog covers natively.', + 'Scanning for competitive SDKs (Sentry, LaunchDarkly, Mixpanel, Datadog, OpenTelemetry, GA4) and PostHog coverage gaps across 8 product surfaces.', + '8 subagents in two waves of 4. Each one returns one of: cross-sell, greenfield, gap, or pass.', + ], + visual: , + docsUrl: 'https://posthog.com/docs', +}; diff --git a/src/ui/tui/screens/audit-3000/slides/featureFlags.tsx b/src/ui/tui/screens/audit-3000/slides/featureFlags.tsx new file mode 100644 index 00000000..9d1cffa1 --- /dev/null +++ b/src/ui/tui/screens/audit-3000/slides/featureFlags.tsx @@ -0,0 +1,33 @@ +import { Text } from 'ink'; +import { VisualBox, type AreaSlide } from '../../audit/slides/shared.js'; + +const FeatureFlagsVisual = () => ( + + + {'new-checkout-v2 '} + {'no code refs '} + {'DROP'} + + + {'beta-dashboard '} + {'1 ref, 100% on '} + {'REVIEW'} + + + {'killswitch-payments'} + {'live experiment'} + {'KEEP'} + + +); + +export const FeatureFlagsSlide: AreaSlide = { + area: 'Feature Flags', + intro: [ + 'LEVEL 6: STALE FLAGS. Old flags add evaluation overhead and confuse the next engineer who wonders if a flag is still live.', + "Cross-referencing PostHog's stale-flag classification against your source tree. Each flag scored: safe-to-disable, needs-review, or unknown.", + 'The final report ships with a copy-paste cleanup prompt. We never touch a flag.', + ], + visual: , + docsUrl: 'https://posthog.com/docs/feature-flags', +}; diff --git a/src/ui/tui/screens/audit-3000/slides/index.ts b/src/ui/tui/screens/audit-3000/slides/index.ts new file mode 100644 index 00000000..e8bd32c5 --- /dev/null +++ b/src/ui/tui/screens/audit-3000/slides/index.ts @@ -0,0 +1,24 @@ +/** + * Audit-3000 slide registry. Re-uses the original audit slides for the + * shared areas (Installation, Identification, Event Capture) and adds + * arcade-flavoured slides for the three new areas the v3000 audit covers. + */ + +import type { AreaSlide } from '../../audit/slides/shared.js'; +import { InstallationSlide } from '../../audit/slides/installation.js'; +import { IdentificationSlide } from '../../audit/slides/identification.js'; +import { EventCaptureSlide } from '../../audit/slides/eventCapture.js'; +import { EventQualitySlide } from './eventQuality.js'; +import { FeatureFlagsSlide } from './featureFlags.js'; +import { ExpansionSlide } from './expansion.js'; + +export type { AreaSlide }; + +export const AUDIT_3000_AREA_SLIDES: AreaSlide[] = [ + InstallationSlide, + IdentificationSlide, + EventCaptureSlide, + EventQualitySlide, + FeatureFlagsSlide, + ExpansionSlide, +]; From 2c885b4197f32f55dd9b4634b1ad3b7b7aa2365b Mon Sep 17 00:00:00 2001 From: Jon <148268521+consultingsultan@users.noreply.github.com> Date: Thu, 14 May 2026 11:05:35 -0400 Subject: [PATCH 3/8] fix(audit-3000): render ellipsis correctly in outro pending state `\u2026` inside a JSX text node renders literally; wrap in `{'...'}` so React resolves the escape. Generated-By: PostHog Code Task-Id: 598dae15-1b52-45e2-8c14-e59aafb1ab9b --- src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx b/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx index 8c965380..d8fe7d41 100644 --- a/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx +++ b/src/ui/tui/screens/audit-3000/Audit3000OutroScreen.tsx @@ -137,7 +137,7 @@ export const Audit3000OutroScreen = ({ store }: Audit3000OutroScreenProps) => { if (!outroData) { return ( - Counting your tokens\u2026 + {'Counting your tokens\u2026'} ); } From ea89ece1942efd9cdfa38a81d0c3f463fcd278e9 Mon Sep 17 00:00:00 2001 From: leonhardprinz Date: Thu, 14 May 2026 11:25:33 -0400 Subject: [PATCH 4/8] feat(audit-3000): merge session-replay seed + audit-plan UX - Seed: +8 session-replay checks (Dana, step 6b) and +3 phase markers for the post-flags chain; total 34 across 9 areas. - Hi-score Table tab: group checks by area with sub-headers, dynamic subtitle, pretty-print JSON details, top summary line. Mirrors the final report layout. - Intro: bump to "34 checks. 9 levels", call out the Hi-score Table tab as the live report and the exported posthog-audit-3000-report.md path. Generated-By: PostHog Code Task-Id: 1f2601d8-e5b5-434e-9ef7-29d996a2cdb9 --- src/lib/workflows/audit-3000/index.ts | 78 +++++++++++++- .../audit-3000/Audit3000IntroScreen.tsx | 29 ++++- .../audit/AuditChecksViewer/AreaHeaderRow.tsx | 23 ++++ .../AuditChecksViewer/AuditChecksViewer.tsx | 100 +++++++++--------- .../audit/AuditChecksViewer/DetailRow.tsx | 72 ++++++++++--- .../screens/audit/AuditChecksViewer/layout.ts | 6 +- .../screens/audit/AuditChecksViewer/sort.ts | 56 ++++++++++ 7 files changed, 287 insertions(+), 77 deletions(-) create mode 100644 src/ui/tui/screens/audit/AuditChecksViewer/AreaHeaderRow.tsx diff --git a/src/lib/workflows/audit-3000/index.ts b/src/lib/workflows/audit-3000/index.ts index d8ac6b90..522df9fa 100644 --- a/src/lib/workflows/audit-3000/index.ts +++ b/src/lib/workflows/audit-3000/index.ts @@ -20,8 +20,10 @@ const AUDIT3000_REPORT_FILE = 'posthog-audit-3000-report.md'; // Extra checks the v3000 audit adds on top of the base 10. IDs must match // those referenced in the audit-3000 skill's step files (Event Quality, -// stale feature-flag review, and per-product use-case expansion). +// stale feature-flag review, session replay [fix + optimize], per-product +// use-case expansion, and phase markers for the post-flags chain). const AUDIT3000_EXTRA_CHECKS: AuditCheck[] = [ + // ── Event Quality (Step 5) ── { id: 'event-naming-standardization', area: 'Event Quality', @@ -46,12 +48,64 @@ const AUDIT3000_EXTRA_CHECKS: AuditCheck[] = [ label: 'Captured events match insights / dashboards usage', status: 'pending', }, + // ── Feature Flags (Step 6) ── { id: 'stale-feature-flags-reviewed', area: 'Feature Flags', label: 'Stale feature flags reviewed', status: 'pending', }, + // ── Session Replay — fix (Step 6b) ── + { + id: 'replay-minimum-duration-set', + area: 'Session Replay', + label: 'Minimum duration set on init', + status: 'pending', + }, + { + id: 'replay-mask-config', + area: 'Session Replay', + label: 'Mask config covers sensitive surfaces', + status: 'pending', + }, + { + id: 'replay-disabled-in-test-envs', + area: 'Session Replay', + label: 'Disabled in test / CI environments', + status: 'pending', + }, + { + id: 'replay-strict-minimum-duration', + area: 'Session Replay', + label: 'Strict minimum duration enforced', + status: 'pending', + }, + // ── Session Replay — optimize (Step 6b cost wave) ── + { + id: 'replay-sampling-rate', + area: 'Session Replay — Optimize', + label: 'Sampling rate tuned for cost', + status: 'pending', + }, + { + id: 'replay-triggers-configured', + area: 'Session Replay — Optimize', + label: 'Triggers configured (event / URL / flag)', + status: 'pending', + }, + { + id: 'replay-network-recording-filtered', + area: 'Session Replay — Optimize', + label: 'Network recording filtered', + status: 'pending', + }, + { + id: 'replay-mobile-sampling', + area: 'Session Replay — Optimize', + label: 'Mobile sampling configured', + status: 'pending', + }, + // ── Use Case: Expansion (Step 9) ── { id: 'expansion-product-analytics', area: 'Use Case: Expansion', @@ -100,6 +154,28 @@ const AUDIT3000_EXTRA_CHECKS: AuditCheck[] = [ label: 'Web analytics coverage', status: 'pending', }, + // ── Additional Sections (Steps 7, 8, 10 phase markers) ── + // Tracked in the ledger so the UI can surface "did it run / was it + // skipped" alongside the regular checks. use-case-expansion is omitted + // because the eight `expansion-*` checks above cover that phase. + { + id: 'customer-enrichment', + area: 'Additional Sections', + label: 'Customer enrichment (Harmonic + PDL)', + status: 'pending', + }, + { + id: 'use-case-match', + area: 'Additional Sections', + label: 'Use-case match', + status: 'pending', + }, + { + id: 'final-report', + area: 'Additional Sections', + label: 'Final audit report written', + status: 'pending', + }, ]; const AUDIT3000_SEED_CHECKS: AuditCheck[] = [ diff --git a/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx b/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx index de27e906..1f64e6b8 100644 --- a/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx +++ b/src/ui/tui/screens/audit-3000/Audit3000IntroScreen.tsx @@ -94,12 +94,25 @@ export const Audit3000IntroScreen = ({ store }: Audit3000IntroScreenProps) => { {AUDIT3000_SKILL_ID} {' '} - workflow reviews your PostHog integration across 23 checks — SDK + workflow reviews your PostHog integration across 34 checks — SDK install, identification, event capture, event quality, stale feature - flag hygiene, and use-case expansion across 8 PostHog products. When - enrichment is available it also produces a company profile and use-case - match. Nothing in your project is modified. + flag hygiene, session replay (fix + optimize), and use-case expansion + across 8 PostHog products. When enrichment is available it also produces + a company profile and use-case match. Nothing in your project is + modified. + + + Results stream live to the{' '} + + Hi-score Table + {' '} + tab during the run — that's your live report. When the audit + finishes, the same report is also exported to{' '} + ./posthog-audit-3000-report.md in your + project folder. + + { - 23 checks. 6 levels. 1 final report. + 34 checks. 9 levels. 1 final report. High-score your PostHog integration before the boss fight. + + + Live report: Hi-score Table tab · + Export: ./posthog-audit-3000-report.md + + ); diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/AreaHeaderRow.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/AreaHeaderRow.tsx new file mode 100644 index 00000000..4b2968ad --- /dev/null +++ b/src/ui/tui/screens/audit/AuditChecksViewer/AreaHeaderRow.tsx @@ -0,0 +1,23 @@ +import { Box, Text } from 'ink'; + +interface AreaHeaderRowProps { + area: string; + resolved: number; + total: number; +} + +/** Sub-header row inside the scrollable body — one per area group. */ +export const AreaHeaderRow = ({ + area, + resolved, + total, +}: AreaHeaderRowProps) => ( + + + {area}{' '} + + + ({resolved}/{total}) + + +); diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx index 45d663fe..e8560ca3 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx +++ b/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx @@ -1,9 +1,10 @@ /** * AuditChecksViewer — "Audit plan" tab. * - * Renders the full audit ledger as a scrollable table grouped by status: - * resolved checks (issues + passes, sorted by severity) on top, pending - * checks at the bottom, separated by a blank row. + * Renders the full audit ledger as a scrollable, area-grouped list that + * mirrors the structure of the final report. Each area gets a sub-header + * with a resolved/total count; checks within an area are sorted by + * severity (error → warning → suggestion → pass → pending). * * Two interactions, both registered via `useKeyBindings`: * - `e` — toggle detail rows (file:line + agent's `details` text) @@ -23,12 +24,13 @@ import { type KeyBinding, } from '../../../hooks/useKeyBindings.js'; import type { AuditCheck } from '../../../../../lib/workflows/audit/types.js'; +import { AreaHeaderRow } from './AreaHeaderRow.js'; import { CheckRow } from './CheckRow.js'; import { DetailRow } from './DetailRow.js'; import { Footer } from './Footer.js'; -import { Header, statusCounts } from './Header.js'; +import { Header, Summary, statusCounts } from './Header.js'; import { computeLayout } from './layout.js'; -import { sortChecks } from './sort.js'; +import { groupChecksByArea } from './sort.js'; interface AuditChecksViewerProps { checks: AuditCheck[]; @@ -36,61 +38,51 @@ interface AuditChecksViewerProps { export const AuditChecksViewer = ({ checks }: AuditChecksViewerProps) => { // ── Layout ───────────────────────────────────────────────────────── - // Recompute on every render against current terminal size so the viewer - // reflows on resize. `viewerChrome` is the row count consumed by header, - // dividers, scroll markers, legend, and counts; `visibleHeight` is what - // remains for the scrollable body. const [rawCols, termRows] = useStdoutDimensions(); const layout = computeLayout(rawCols, termRows); const totalHeight = layout.visibleHeight + layout.viewerChrome; - // ── Sort + section split ─────────────────────────────────────────── - // Issues + passes on top, pending at the bottom. The JSX renders the - // two sections in that order with a blank-line separator between them. - const sorted = useMemo(() => sortChecks(checks), [checks]); - const resolved = sorted.filter((c) => c.status !== 'pending'); - const pending = sorted.filter((c) => c.status === 'pending'); + // ── Group by area ────────────────────────────────────────────────── + const groups = useMemo(() => groupChecksByArea(checks), [checks]); + const counts = useMemo(() => statusCounts(checks), [checks]); // ── Expand state ─────────────────────────────────────────────────── - const hasExpandable = sorted.some((c) => Boolean(c.details || c.file)); - const hasIssues = sorted.some( + const hasExpandable = checks.some((c) => Boolean(c.details || c.file)); + const hasIssues = checks.some( (c) => c.status === 'error' || c.status === 'warning' || c.status === 'suggestion', ); - // Auto-expand when there are issues — the AuditAreaPane's `[→] View - // issues` hint sends users here specifically to read details. const [expanded, setExpanded] = useState(hasIssues && hasExpandable); // ── Flat row list ────────────────────────────────────────────────── - // Build one ReactNode per visible terminal row so scroll math is a - // single number. CheckRow = 1 row; an expanded DetailRow ≈ 1 (long - // details that wrap will overflow — we don't track exact heights, the - // approximation is fine for a status pane). + // One ReactNode per visible terminal row so scroll math stays simple. + // Sub-header + check rows + (optional) detail rows interleave here. const allRows = useMemo(() => { const rows: ReactNode[] = []; - const buildRow = (item: AuditCheck) => { - rows.push(); - if (expanded && (item.details || item.file)) { - rows.push( - , - ); + for (const group of groups) { + rows.push( + , + ); + for (const item of group.checks) { + rows.push(); + if (expanded && (item.details || item.file)) { + rows.push( + , + ); + } } - }; - resolved.forEach(buildRow); - if (resolved.length > 0 && pending.length > 0) { - rows.push(); } - pending.forEach(buildRow); return rows; - }, [resolved, pending, expanded, layout]); + }, [groups, expanded, layout]); // ── Scroll viewport ──────────────────────────────────────────────── - // `offset` is the index of the first visible row. Clamped on every - // render so resizing or collapsing details can't leave us scrolled - // past the new end. `hiddenAbove` / `hiddenBelow` drive the - // "↑ N more" / "↓ N more" markers above and below the body. const [offset, setOffset] = useState(0); const maxOffset = Math.max(0, allRows.length - layout.visibleHeight); const clampedOffset = Math.min(offset, maxOffset); @@ -101,10 +93,6 @@ export const AuditChecksViewer = ({ checks }: AuditChecksViewerProps) => { ); // ── Key bindings ─────────────────────────────────────────────────── - // `e` toggles detail rows (only registered when there's something to - // expand). `↑`/`↓` always register so the hints bar consistently - // advertises scroll, even when content fits and the handler is a - // no-op via the clamp. const bindings: KeyBinding[] = []; if (hasExpandable) { bindings.push({ @@ -131,21 +119,29 @@ export const AuditChecksViewer = ({ checks }: AuditChecksViewerProps) => { clampedOffset + layout.visibleHeight, ); + // Dynamic subtitle — lists the actual areas in the ledger. + const subtitle = + groups.length === 0 + ? 'No checks yet.' + : `Review across ${groups.length} ${ + groups.length === 1 ? 'area' : 'areas' + } — mirrors the final report.`; + return ( - {/* Title */} + {/* Title + dynamic subtitle */} Audit plan - - Read-only review of installation, identification, and event capture - + {subtitle} + + {/* Top summary — same as Footer summary, promoted here for at-a-glance */} + {/* Column headers + divider */}
{'─'.repeat(layout.dividerWidth)} - {/* Scroll-up marker (renders a blank row when nothing is hidden - above so the body's vertical position stays stable) */} + {/* Scroll-up marker */} {hiddenAbove > 0 ? `↑ ${hiddenAbove} more` : ' '} {/* Scrollable body */} @@ -159,11 +155,11 @@ export const AuditChecksViewer = ({ checks }: AuditChecksViewerProps) => { ))} - {/* Scroll-down marker (mirror of the above) */} + {/* Scroll-down marker */} {hiddenBelow > 0 ? `↓ ${hiddenBelow} more` : ' '} - {/* Legend + count summary */} -