diff --git a/src/programs/agent-skill/index.ts b/src/programs/agent-skill/index.ts index ad6572fc1..ded8be086 100644 --- a/src/programs/agent-skill/index.ts +++ b/src/programs/agent-skill/index.ts @@ -1 +1,32 @@ -export { AGENT_SKILL_STEPS } from '../../tui/programs/shared/skill-flow.js'; +import type { ProgramConfig } from '../program-step.js'; +import { POSTHOG_DOCS_URL } from '@shared/constants.js'; +import { AGENT_SKILL_SCOPE_ADDITIONS } from './scopes.js'; + +// Generic skill program — runs an arbitrary context-mill skill chosen at +// dispatch time (session.skillId) rather than a registered named program. +// Backs `wizard skill ` and the narrow `audit` leaves (events, +// feature-flags, identify, session-replay, autocapture); each injects its +// skillId onto the config, which lands on session.skillId before the run. +// +// The `run` recipe is a function rather than a static block because the +// skillId isn't known until dispatch. Without a `run` recipe runProgram fails +// with "has no run configuration" and the skill never executes — so we derive +// generic run metadata from the resolved skill id at run time. +export const config: ProgramConfig = { + id: 'agent-skill', + description: 'Run an arbitrary context-mill skill', + allowedTools: ['Agent'], + oauthScopeAdditions: AGENT_SKILL_SCOPE_ADDITIONS, + run: (session) => { + const skillId = session.skillId ?? 'agent-skill'; + return Promise.resolve({ + skillId, + integrationLabel: skillId, + spinnerMessage: `Running ${skillId}...`, + successMessage: `${skillId} complete!`, + estimatedDurationMinutes: 5, + reportFile: `posthog-${skillId}-report.md`, + docsUrl: POSTHOG_DOCS_URL, + }); + }, +}; diff --git a/src/programs/ai-observability/index.ts b/src/programs/ai-observability/index.ts index cbbf99265..3d9c78b6c 100644 --- a/src/programs/ai-observability/index.ts +++ b/src/programs/ai-observability/index.ts @@ -1,12 +1,7 @@ -import type { ProgramConfig, ProgramStep } from '@programs/program-step'; -import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; -import { getContentBlocks } from '@tui/programs/shared/skill-deck'; +import { Harness, Sequence, GPT5_6_TERRA_MODEL } from '@shared/constants'; +import type { ProgramConfig } from '../program-step'; import { headlessOption, regionOption } from '@shared/headless-mode'; -const AI_OBSERVABILITY_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => - step.id === 'intro' ? { ...step, screenId: 'ai-observability-intro' } : step, -); - const AI_OBSERVABILITY_REPORT_FILE = 'posthog-ai-observability-report.md'; /** @@ -19,14 +14,18 @@ const AI_OBSERVABILITY_REPORT_FILE = 'posthog-ai-observability-report.md'; * the right variant itself (see `customPrompt`). Stays flat while a single * "add AIO to a project" flow is the only action. */ -export const aiObservabilityConfig: ProgramConfig = { +export const config: ProgramConfig = { + binding: { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_TERRA_MODEL, + thinkingLevel: 'high', + }, command: 'ai-observability', description: 'Add PostHog AI Observability to your LLM calls', id: 'ai-observability', cliOptions: { ...headlessOption, ...regionOption }, - steps: AI_OBSERVABILITY_STEPS, reportFile: AI_OBSERVABILITY_REPORT_FILE, - getContentBlocks, run: { integrationLabel: 'ai-observability', // No `skillId`: linear.ts skips its pre-install step (see the gate on diff --git a/src/programs/audit/events/config.ts b/src/programs/audit/events/config.ts index db80f6085..3eb2cfb82 100644 --- a/src/programs/audit/events/config.ts +++ b/src/programs/audit/events/config.ts @@ -1,13 +1,12 @@ -import type { ProgramConfig } from '@programs/program-step'; -import type { ProgramRun } from '@programs/program-run'; -import type { WizardSession } from '@programs/session/wizard-session'; -import { OutroKind } from '@programs/session/wizard-session'; -import { SPINNER_MESSAGE } from '@programs/framework-config'; +import type { ProgramConfig } from '../../program-step'; +import type { ProgramRun } from '../../program-run'; +import type { ProgramSession } from '../../program-session'; +import { OutroKind } from '@shared/outro'; +import { SPINNER_MESSAGE } from '../../framework-config'; import { isUsingTypeScript } from '@utils/setup-utils'; import { WIZARD_TOOL_NAMES } from '@agent'; -import { EVENTS_AUDIT_PROGRAM } from '../../../tui/programs/audit/events-flow.js'; -import { AUDIT_CHECKS_FILE, AUDIT_CHECKS_KEY } from '@programs/audit/types'; -import { seedAuditLedger } from '@programs/audit/seed'; +import { AUDIT_CHECKS_FILE, AUDIT_CHECKS_KEY } from '../types.js'; +import { seedAuditLedger } from '../seed.js'; import { EVENTS_AUDIT_SEED_CHECKS } from './seed.js'; // SETUP_REPORT_FILE is also re-exported for backward compat with existing @@ -25,11 +24,10 @@ const DOCS_URL = 'https://posthog.com/docs/product-analytics/best-practices'; * skill (whose id AuditRunScreen keys its slides on), not to this config. * Registered so its id stays resolvable; nothing dispatches to it today. */ -export const eventsAuditConfig: ProgramConfig = { +export const config: ProgramConfig = { description: 'Audit PostHog event tracking in this project', id: 'events-audit', skillId: 'events-audit', - steps: EVENTS_AUDIT_PROGRAM, // Top-level reportFile so AuditRunScreen can resolve the report path // synchronously without unwrapping the deferred `run` function. reportFile: SETUP_REPORT_FILE, @@ -42,7 +40,7 @@ export const eventsAuditConfig: ProgramConfig = { ], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], - run: (session: WizardSession): Promise => { + run: (session: ProgramSession): Promise => { const typeScriptDetected = isUsingTypeScript({ installDir: session.installDir, }); @@ -105,5 +103,3 @@ Project context: }); }, }; - -export { EVENTS_AUDIT_PROGRAM } from '../../../tui/programs/audit/events-flow.js'; diff --git a/src/programs/audit/events/constants.ts b/src/programs/audit/events/constants.ts index 8d381e65c..57c67de41 100644 --- a/src/programs/audit/events/constants.ts +++ b/src/programs/audit/events/constants.ts @@ -1,6 +1,6 @@ /** * Leaf-level constants for the events-audit program. The document names live - * in `@lib/constants` so infrastructure (the scanner's allowlist) can name + * in `@shared/constants` so infrastructure (the scanner's allowlist) can name * them without importing this program. */ diff --git a/src/programs/audit/events/seed.ts b/src/programs/audit/events/seed.ts index ef480aac0..a3f441c99 100644 --- a/src/programs/audit/events/seed.ts +++ b/src/programs/audit/events/seed.ts @@ -1,4 +1,4 @@ -import type { AuditCheck } from '@programs/audit/types'; +import type { AuditCheck } from '../types.js'; /** * The 7 phases the events-audit skill marches through. One check per area diff --git a/src/programs/audit/index.ts b/src/programs/audit/index.ts index a5ce0e3b5..6cc505bb0 100644 --- a/src/programs/audit/index.ts +++ b/src/programs/audit/index.ts @@ -1,11 +1,9 @@ -import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; -import { createSkillProgram } from '@programs/shared/skill-program'; -import { withAuditScreens } from '@tui/programs/audit/flow'; -import type { ProgramStep, ProgramConfig } from '@programs/program-step'; -import type { ProgramRun } from '@programs/program-run'; -import type { RunnerContext } from '@programs/runner-context'; -import type { WizardSession } from '@programs/session/wizard-session'; -import { OutroKind } from '@programs/session/wizard-session'; +import { createSkillProgram } from '../shared/skill-program'; +import type { ProgramConfig } from '../program-step'; +import type { ProgramRun } from '../program-run'; +import type { RunnerContext } from '../runner-context'; +import type { ProgramSession } from '../program-session'; +import { OutroKind } from '@shared/outro'; import { WIZARD_TOOL_NAMES } from '@agent'; import { headlessOption, regionOption } from '@shared/headless-mode'; import { AUDIT_ABORT_CASES } from './detect.js'; @@ -15,14 +13,13 @@ import { AUDIT_REPORT_FILE, } from './types.js'; import { AUDIT_SEED_CHECKS, seedAuditLedger } from './seed.js'; +import { config as eventsAudit } from './events/config.js'; -const seedBeforeAuditRun = (session: WizardSession): void => { +const seedBeforeAuditRun = (session: ProgramSession): void => { seedAuditLedger(session.installDir); session.frameworkContext[AUDIT_CHECKS_KEY] = AUDIT_SEED_CHECKS; }; -const auditSteps: ProgramStep[] = withAuditScreens(AGENT_SKILL_STEPS); - const baseConfig = createSkillProgram({ skillId: 'audit', command: 'audit', @@ -42,7 +39,7 @@ const baseConfig = createSkillProgram({ }); const auditRun = async ( - session: WizardSession, + session: ProgramSession, runner: RunnerContext, ): Promise => { seedBeforeAuditRun(session); @@ -67,12 +64,8 @@ const auditRun = async ( ? `${cloudUrl}/products?source=wizard` : undefined; - // Note: `sess` here is the agent-runner's snapshot of session at - // runAgent() invocation time. Any URL emissions during the run land - // on the live store, NOT on this snapshot. The UI layer - // (InkUI.setOutroData) merges live URLs in on top of this return - // value, so it's safe to leave dashboardUrl/notebookUrl as undefined - // here when the snapshot doesn't have them. + // The session store lays any URL the agent emitted during the run over + // this outro, so dashboardUrl/notebookUrl may stay undefined here. return { kind: OutroKind.Success as const, message: baseRun.successMessage, @@ -86,9 +79,8 @@ const auditRun = async ( }; }; -export const auditConfig: ProgramConfig = { +const audit: ProgramConfig = { ...baseConfig, - steps: auditSteps, run: auditRun, auditLedgerFile: AUDIT_CHECKS_FILE, // Ledger tools are opt-in per program; pi matches on the short name. @@ -104,3 +96,24 @@ export const auditConfig: ProgramConfig = { // `wizard audit` command; dispatchProgram routes it to runWizardHeadless. cliOptions: { ...headlessOption, ...regionOption }, }; + +/** Registers `audit` and `events-audit`, which has no entry of its own. */ +export const configs = [ + audit, + eventsAudit, +] as const satisfies readonly ProgramConfig[]; + +export { + AUDIT_CHECKS_FILE, + AUDIT_CHECKS_KEY, + AUDIT_REPORT_FILE, + getAuditChecks, + type AuditCheck, + type AuditStatus, +} from './types.js'; +export { + removeAuditLedger, + startAuditLedgerWatcher, +} from './ledger-watcher.js'; +/** The checks every audit run starts from. */ +export { AUDIT_SEED_CHECKS } from './seed.js'; diff --git a/src/programs/audit/ledger-watcher.ts b/src/programs/audit/ledger-watcher.ts index 56f314aa8..1dba5c055 100644 --- a/src/programs/audit/ledger-watcher.ts +++ b/src/programs/audit/ledger-watcher.ts @@ -1,26 +1,27 @@ /** * Mirrors the agent's `.posthog-audit-checks.json` into the session, so the TUI - * screens and the task stream read one value. `runProgramAgent` owns the + * screens and the task stream read one value. `runProgram` owns the * lifecycle and removes the file at run end, so every path gets it — including * the e2e host, which builds no task stream. */ import fs from 'fs'; import path from 'path'; -import { getUI } from '@ui'; +import type { RunnerContext } from '../runner-context'; import { startFileWatcher, type FileWatcherHandle, type FileWatcherOptions, -} from '@shared/utils/file-watcher'; +} from '@utils/file-watcher'; import { logToFile } from '@utils/debug'; -import { AUDIT_CHECKS_KEY, coerceAuditChecks } from './types.js'; +import { AUDIT_CHECKS_KEY, coerceAuditChecks } from './types'; const MAX_LEDGER_FILE_BYTES = 256 * 1024; export function startAuditLedgerWatcher( installDir: string, file: string, + runner: Pick, options: FileWatcherOptions = {}, ): FileWatcherHandle { const target = path.join(installDir, file); @@ -29,7 +30,7 @@ export function startAuditLedgerWatcher( return startFileWatcher( target, (parsed) => - getUI().setFrameworkContext(AUDIT_CHECKS_KEY, coerceAuditChecks(parsed)), + runner.setFrameworkContext(AUDIT_CHECKS_KEY, coerceAuditChecks(parsed)), { // A ledger an earlier run left behind stays ignored until this run writes. ignoreInitialFile: true, diff --git a/src/programs/audit/types.ts b/src/programs/audit/types.ts index b0fd7fd50..0e93093ad 100644 --- a/src/programs/audit/types.ts +++ b/src/programs/audit/types.ts @@ -6,7 +6,7 @@ import { type AuditStatus, } from '@shared/audit-ledger'; -// The ledger contract lives in `@lib/audit-ledger`; re-exported so the audit +// The ledger contract lives in `@shared/audit-ledger`; re-exported so the audit // views and the ledger watcher keep their import path. export { AUDIT_CHECKS_FILE, AUDIT_REPORT_FILE, coerceAuditChecks }; export type { AuditCheck, AuditStatus }; diff --git a/src/programs/error-tracking-upload-source-maps/detect-agentic.ts b/src/programs/error-tracking-upload-source-maps/detect-agentic.ts index ded3a8cc3..077502608 100644 --- a/src/programs/error-tracking-upload-source-maps/detect-agentic.ts +++ b/src/programs/error-tracking-upload-source-maps/detect-agentic.ts @@ -16,8 +16,9 @@ import { type DetectTarget, type AgenticDetectionReport, type DetectEvent, -} from '@programs/detection/agentic'; -import type { WizardSession } from '@programs/session/wizard-session'; + type DetectProgress, +} from '../detection/agentic'; +import type { ProgramSession } from '../program-session'; import { VARIANT_DISPLAY_NAME, AUTOMATABLE_VARIANTS, @@ -401,8 +402,9 @@ export function coerceReport( /** Run the Haiku detector over the repo and classify projects for source maps. */ export async function detectSourceMapsProjects( - session: WizardSession, + session: ProgramSession, onEvent?: DetectEvent, + onProgress?: DetectProgress, ): Promise { const report = await detectProjectsWithAgent(session, { targets: SOURCE_MAPS_TARGETS, @@ -410,6 +412,7 @@ export async function detectSourceMapsProjects( purpose: 'set up PostHog Error Tracking source-map upload', rerankIds: JS_RERANK_VARIANTS, onEvent, + onProgress, }); return toSourceMapsReport(report, { hasBuildTarget: (path) => projectHasBuildTarget(session.installDir, path), diff --git a/src/programs/error-tracking-upload-source-maps/detect.ts b/src/programs/error-tracking-upload-source-maps/detect.ts index c39d7443b..3562b31f1 100644 --- a/src/programs/error-tracking-upload-source-maps/detect.ts +++ b/src/programs/error-tracking-upload-source-maps/detect.ts @@ -16,7 +16,7 @@ import { MAX_WALK_FILES, safeReadFile, } from '@utils/bounded-fs'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { ProgramSession } from '../program-session'; import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; @@ -357,7 +357,7 @@ export const SOURCE_MAPS_CONTEXT_KEYS = { * only picks which variant the prompt should ask the agent to load. */ export function detectSourceMapsPrerequisites( - session: WizardSession, + session: Pick, setFrameworkContext: (key: string, value: unknown) => void, ): void { const fail = (error: SourceMapsDetectError) => diff --git a/src/programs/error-tracking-upload-source-maps/index.ts b/src/programs/error-tracking-upload-source-maps/index.ts index 58f8161ec..3cfa17820 100644 --- a/src/programs/error-tracking-upload-source-maps/index.ts +++ b/src/programs/error-tracking-upload-source-maps/index.ts @@ -1,9 +1,8 @@ -import type { ProgramConfig } from '@programs/program-step'; -import type { ProgramRun } from '@programs/program-run'; -import type { WizardSession } from '@programs/session/wizard-session'; -import { OutroKind } from '@programs/session/wizard-session'; -import type { RunnerContext } from '@programs/runner-context'; -import { ERROR_TRACKING_UPLOAD_SOURCE_MAPS_PROGRAM } from '../../tui/programs/error-tracking-upload-source-maps/flow.js'; +import type { ProgramConfig } from '../program-step'; +import type { ProgramRun } from '../program-run'; +import type { ProgramSession } from '../program-session'; +import { OutroKind } from '@shared/outro'; +import type { RunnerContext } from '../runner-context'; import { buildSourceMapsUploadPrompt, SOURCE_MAPS_DETECTION_FAILED_PROMPT, @@ -14,8 +13,7 @@ import { VARIANTS_REQUIRING_POSTHOG_CLI, type SkillVariant, } from './detect.js'; -import { getContentBlocks } from '../../tui/programs/shared/deck/source-maps.js'; -import { preinstallPostHogCliOnce } from '@programs/shared/posthog-cli-preinstall'; +import { preinstallPostHogCliOnce } from '../shared/posthog-cli-preinstall'; const REPORT_FILE = 'posthog-source-maps-report.md'; const DOCS_URL = 'https://posthog.com/docs/error-tracking/upload-source-maps'; @@ -36,18 +34,17 @@ function ensurePostHogCli( ); } -export const errorTrackingUploadSourceMapsConfig: ProgramConfig = { +export const config: ProgramConfig = { command: 'upload-source-maps', description: 'Upload source maps to PostHog Error Tracking', id: 'error-tracking-upload-source-maps', - requiresAi: true, - steps: ERROR_TRACKING_UPLOAD_SOURCE_MAPS_PROGRAM, + // No health-check screen in the TUI flow; the run skips the readiness check. + healthCheck: false, reportFile: REPORT_FILE, - getContentBlocks, requires: ['posthog-integration'], run: ( - _session: WizardSession, + _session: ProgramSession, runner: RunnerContext, ): Promise => { // Read the picked project LIVE at prompt-build time, not here: the picker @@ -136,13 +133,19 @@ export const errorTrackingUploadSourceMapsConfig: ProgramConfig = { }, }; -export { ERROR_TRACKING_UPLOAD_SOURCE_MAPS_PROGRAM } from '../../tui/programs/error-tracking-upload-source-maps/flow.js'; export { detectSourceMapsPrerequisites, SOURCE_MAPS_ABORT_CASES, SOURCE_MAPS_CONTEXT_KEYS, VARIANT_DISPLAY_NAME, + VARIANTS_REQUIRING_POSTHOG_CLI, MANUAL_SDK_VARIANTS, type SkillVariant, type SourceMapsDetectError, } from './detect.js'; + +export { + detectSourceMapsProjects, + type DetectedProject, + type DetectionReport, +} from './detect-agentic.js'; diff --git a/src/programs/error-tracking/detect-agentic.ts b/src/programs/error-tracking/detect-agentic.ts index 3e6d14551..844e2094d 100644 --- a/src/programs/error-tracking/detect-agentic.ts +++ b/src/programs/error-tracking/detect-agentic.ts @@ -16,13 +16,16 @@ import { resolveProjectDir, type AgenticDetectionReport, type DetectEvent, -} from '@programs/detection/agentic'; -import { gatherFrameworkContext } from '@programs/detection/index'; + type DetectProgress, +} from '../detection/agentic'; +import { gatherFrameworkContext } from '../detection/context'; +import { noteDetectedFramework } from '../detection/detected-framework'; import { detectIntegrationProjects, toIntegrationCandidates, -} from '@programs/detection/project-scope'; -import type { WizardSession } from '@programs/session/wizard-session'; +} from '../detection/project-scope'; +import type { ProgramSession } from '../program-session'; +import type { RunnerContext } from '../runner-context'; /** frameworkContext key for the picked project's path, relative to the repo root. */ export const ERROR_TRACKING_PROJECT_PATH_KEY = 'errorTrackingProjectPath'; @@ -70,19 +73,21 @@ export function toErrorTrackingReport( /** Scan the repo for projects, billed to error tracking. */ export async function detectErrorTrackingProjects( - session: WizardSession, + session: ProgramSession, onEvent?: DetectEvent, + onProgress?: DetectProgress, ): Promise { const report = await detectIntegrationProjects(session, { programId: 'error-tracking', recommend: true, onEvent, + onProgress, }); return toErrorTrackingReport(report); } /** The run's working directory: the picked project, else the repo root. */ -export function errorTrackingProjectDir(session: WizardSession): string { +export function errorTrackingProjectDir(session: ProgramSession): string { return resolveProjectDir( session.installDir, session.frameworkContext[ERROR_TRACKING_PROJECT_PATH_KEY], @@ -91,7 +96,8 @@ export function errorTrackingProjectDir(session: WizardSession): string { /** Gather framework context for `session.installDir`, keeping keys already set. */ export async function gatherErrorTrackingContext( - session: WizardSession, + session: ProgramSession, + log: RunnerContext['log'], ): Promise { const frameworkConfig = session.frameworkConfig; if (!frameworkConfig) return; @@ -108,4 +114,5 @@ export async function gatherErrorTrackingContext( session.frameworkContext[key] = value; } } + noteDetectedFramework(session, frameworkConfig, context, log); } diff --git a/src/programs/error-tracking/index.ts b/src/programs/error-tracking/index.ts index 126100abd..e286c0664 100644 --- a/src/programs/error-tracking/index.ts +++ b/src/programs/error-tracking/index.ts @@ -1,23 +1,24 @@ -import { Integration } from '@shared/constants'; -import { detectFramework } from '@programs/detection/index'; -import { scopeInstallDirToProject } from '@programs/detection/project-scope'; -import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; -import type { ProgramRun } from '@programs/program-run'; -import { AGENT_SKILL_STEPS } from '@tui/programs/shared/skill-flow'; -import { PICK_PROJECT_STEP } from '@tui/programs/error-tracking/flow'; -import { getContentBlocks } from '@tui/programs/error-tracking/deck/index'; -import { getTips } from '@tui/programs/error-tracking/deck/tips'; +import { + Harness, + Integration, + Sequence, + DEFAULT_AGENT_MODEL, +} from '@shared/constants'; +import { detectFramework } from '../detection/framework'; +import { scopeInstallDirToProject } from '../detection/project-scope'; +import { FRAMEWORK_REGISTRY } from '../frameworks/registry'; +import type { ProgramRun } from '../program-run'; import { ERROR_TRACKING_UNSUPPORTED, errorTrackingProjectDir, gatherErrorTrackingContext, -} from '@programs/error-tracking/detect-agentic'; -import type { ProgramConfig, ProgramStep } from '@programs/program-step'; -import type { WizardSession } from '@programs/session/wizard-session'; -import type { CiRunnerContext, RunnerContext } from '@programs/runner-context'; -import { preinstallPostHogCliOnce } from '@programs/shared/posthog-cli-preinstall'; +} from './detect-agentic'; +import type { ProgramConfig } from '../program-step'; +import type { ProgramSession } from '../program-session'; +import type { CiRunnerContext, RunnerContext } from '../runner-context'; +import { preinstallPostHogCliOnce } from '../shared/posthog-cli-preinstall'; import { analytics } from '@utils/analytics'; -import { wizardAbort } from '@host/wizard-abort'; +import { ProgramAbort } from '../program-abort'; import { ErrorCodes } from '@shared/errors'; const ERROR_TRACKING_REPORT_FILE = 'posthog-error-tracking-report.md'; @@ -40,15 +41,13 @@ export const SYMBOL_UPLOAD_CLI_FRAMEWORKS: ReadonlySet = new Set([ Integration.rust, ]); -async function abortUnsupportedPlatform( - integration: Integration, -): Promise { +function abortUnsupportedPlatform(integration: Integration): never { const name = FRAMEWORK_REGISTRY[integration]?.metadata.name ?? integration; // A clean exit, not a crash: an event, never an `error` for captureException. analytics.wizardCapture('error tracking unsupported platform', { integration, }); - await wizardAbort({ + throw new ProgramAbort({ code: ErrorCodes.DetectUnsupportedPlatform, message: `The wizard cannot set up error tracking for ${name} projects yet.\n\n` + @@ -57,42 +56,26 @@ async function abortUnsupportedPlatform( } /** - * Pre-install posthog-cli when the detected framework's symbol upload will - * shell out to it. See `preinstallPostHogCliOnce` for the once-per-process - * guard and the warn-don't-fail handling. + * Prepare the run once the project is known: pre-install posthog-cli when the + * framework's symbol upload shells out to it, then gather the framework + * context. See `preinstallPostHogCliOnce` for the once-per-process guard and + * the warn-don't-fail handling. */ -function maybePreinstallPostHogCli( - integration: Integration | null, +async function prepareErrorTrackingRun( + session: ProgramSession, log: RunnerContext['log'], -): void { - if (!integration || !SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(integration)) return; - preinstallPostHogCliOnce( - 'error tracking posthog-cli preinstall failed', - { integration }, - log, - ); +): Promise { + const { integration } = session; + if (integration && SYMBOL_UPLOAD_CLI_FRAMEWORKS.has(integration)) { + preinstallPostHogCliOnce( + 'error tracking posthog-cli preinstall failed', + { integration }, + log, + ); + } + await gatherErrorTrackingContext(session, log); } -const ERROR_TRACKING_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.flatMap( - (step): ProgramStep[] => { - if (step.id === 'intro') { - return [{ ...step, screenId: 'error-tracking-intro' }]; - } - if (step.id === 'auth') return [step, PICK_PROJECT_STEP]; - if (step.id === 'run') { - // targetDir makes run-wizard walk the steps and run in the picked project. - return [ - { - ...step, - targetDir: errorTrackingProjectDir, - onRunPrep: gatherErrorTrackingContext, - }, - ]; - } - return [step]; - }, -); - /** * Run instructions for a linear override (`--sequence=linear`), the only * sequence that reads `customPrompt`. The orchestrator runs the flow's own @@ -155,56 +138,70 @@ const ERROR_TRACKING_RUN: ProgramRun = { * - 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. - * - `PICK_PROJECT_STEP` after auth: the user picks the project, which sets the - * framework preflight needs and the directory the run is scoped to. - * - `run` is a function: `runAgent` resolves it after the pick (and after - * `ciPreRun` headless), so the posthog-cli pre-install, which the agent - * cannot do (warlock blocks \`npm install -g\`), waits for the user. + * - The user picks the project after auth, which sets the framework preflight + * needs and the directory the run is scoped to (`runSteps.run.targetDir`). + * - The run step's `onRunPrep` runs after the pick (`ciPreRun` headless), so + * the posthog-cli pre-install, which the agent cannot do (warlock blocks + * \`npm install -g\`), waits for the project. * - `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 project picker. */ -export const errorTrackingConfig: ProgramConfig = { +export const config: ProgramConfig = { + // 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, install and init, sol tasks, luna report). + binding: { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, command: 'error-tracking', description: 'Set up PostHog error tracking, source-map upload and release linking included', id: 'error-tracking', agentFlow: 'error-tracking', - steps: ERROR_TRACKING_STEPS, reportFile: ERROR_TRACKING_REPORT_FILE, - getContentBlocks, - getTips, - - run: (session: WizardSession, runner: RunnerContext): Promise => { - maybePreinstallPostHogCli(session.integration, runner.log); - return Promise.resolve(ERROR_TRACKING_RUN); + // The run is scoped to the project the user picks after login. + runSteps: { + run: { + targetDir: errorTrackingProjectDir, + onRunPrep: prepareErrorTrackingRun, + }, }, + run: ERROR_TRACKING_RUN, + ciPreRun: async ( - session: WizardSession, + session: ProgramSession, runner: CiRunnerContext, ): Promise => { await scopeInstallDirToProject(session, runner); const integration = await detectFramework(session.installDir); if (!integration) { - await wizardAbort({ + throw new ProgramAbort({ code: ErrorCodes.DetectNoFramework, message: 'Could not auto-detect your framework for this project.', }); - return; } if (ERROR_TRACKING_UNSUPPORTED.has(integration)) { - await abortUnsupportedPlatform(integration); - return; + abortUnsupportedPlatform(integration); } session.integration = integration; analytics.setTag('integration', integration); session.frameworkConfig = FRAMEWORK_REGISTRY[integration]; session.skillId = integration; - await gatherErrorTrackingContext(session); + await prepareErrorTrackingRun(session, runner.log); }, }; + +export { + ERROR_TRACKING_PROJECT_PATH_KEY, + detectErrorTrackingProjects, + type ErrorTrackingDetectionReport, + type ErrorTrackingProject, +} from './detect-agentic.js'; diff --git a/src/programs/frameworks/android/android-wizard-agent.ts b/src/programs/frameworks/android/android-wizard-agent.ts index f2f81227b..31da237ae 100644 --- a/src/programs/frameworks/android/android-wizard-agent.ts +++ b/src/programs/frameworks/android/android-wizard-agent.ts @@ -1,6 +1,6 @@ /* Android (Kotlin) wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; +import type { FrameworkConfig } from '../../framework-config'; import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import * as fs from 'node:fs'; @@ -10,7 +10,7 @@ import { getKotlinVersionBucket, getMinSdkVersion, } from './utils'; -import { gradlePackageManager } from '@programs/detection/package-manager'; +import { gradlePackageManager } from '../../detection/package-manager'; type AndroidContext = { kotlinVersion?: string; diff --git a/src/programs/frameworks/angular/angular-wizard-agent.ts b/src/programs/frameworks/angular/angular-wizard-agent.ts index e746a1851..166e9166a 100644 --- a/src/programs/frameworks/angular/angular-wizard-agent.ts +++ b/src/programs/frameworks/angular/angular-wizard-agent.ts @@ -1,7 +1,7 @@ /* Angular wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/programs/frameworks/astro/astro-wizard-agent.ts b/src/programs/frameworks/astro/astro-wizard-agent.ts index a84ad23b3..6dcd05654 100644 --- a/src/programs/frameworks/astro/astro-wizard-agent.ts +++ b/src/programs/frameworks/astro/astro-wizard-agent.ts @@ -1,7 +1,7 @@ /* Astro wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, @@ -10,7 +10,6 @@ import { type PackageJson, } from '@utils/package-json'; import { tryGetPackageJson } from '@utils/setup-utils'; -import { getUI } from '@ui'; import { getAstroRenderingMode, getAstroVersionBucket, @@ -29,11 +28,12 @@ export const ASTRO_AGENT_CONFIG: FrameworkConfig = { docsUrl: 'https://posthog.com/docs/libraries/astro', gatherContext: async (options: WizardRunOptions) => { const renderingMode = await getAstroRenderingMode(options); - getUI().setDetectedFramework( - `Astro ${getAstroRenderingModeName(renderingMode)}`, - ); return { renderingMode }; }, + getDetectedFrameworkLabel: (context) => + context.renderingMode + ? `Astro ${getAstroRenderingModeName(context.renderingMode)}` + : undefined, }, detection: { diff --git a/src/programs/frameworks/django/django-wizard-agent.ts b/src/programs/frameworks/django/django-wizard-agent.ts index 6c93bbc4e..99abbb5e6 100644 --- a/src/programs/frameworks/django/django-wizard-agent.ts +++ b/src/programs/frameworks/django/django-wizard-agent.ts @@ -1,8 +1,8 @@ /* Django wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; -import { detectPythonPackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '../../framework-config'; +import { detectPythonPackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; @@ -33,6 +33,18 @@ export const DJANGO_AGENT_CONFIG: FrameworkConfig = { const settingsFile = await findDjangoSettingsFile(options); return { projectType, settingsFile }; }, + getDetectedFrameworkLabel: (context) => { + switch (context.projectType) { + case DjangoProjectType.WAGTAIL: + return 'Django with Wagtail CMS'; + case DjangoProjectType.DRF: + return 'Django REST Framework'; + case DjangoProjectType.CHANNELS: + return 'Django Channels'; + case DjangoProjectType.STANDARD: + return 'Django'; + } + }, }, detection: { diff --git a/src/programs/frameworks/django/utils.ts b/src/programs/frameworks/django/utils.ts index 276c2b746..4c6ef112a 100644 --- a/src/programs/frameworks/django/utils.ts +++ b/src/programs/frameworks/django/utils.ts @@ -1,5 +1,4 @@ import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; -import { getUI } from '@ui'; import type { WizardRunOptions } from '@utils/types'; import { createVersionBucket } from '@utils/semver'; import * as fs from 'node:fs'; @@ -155,24 +154,20 @@ export async function getDjangoProjectType( // Check for Wagtail first (CMS) if (await hasWagtail({ installDir })) { - getUI().setDetectedFramework('Django with Wagtail CMS'); return DjangoProjectType.WAGTAIL; } // Check for Django REST Framework if (await hasDRF({ installDir })) { - getUI().setDetectedFramework('Django REST Framework'); return DjangoProjectType.DRF; } // Check for Django Channels if (await hasChannels({ installDir })) { - getUI().setDetectedFramework('Django Channels'); return DjangoProjectType.CHANNELS; } // Default to standard Django - getUI().setDetectedFramework('Django'); return DjangoProjectType.STANDARD; } diff --git a/src/programs/frameworks/elixir/elixir-wizard-agent.ts b/src/programs/frameworks/elixir/elixir-wizard-agent.ts index b617f74b9..c7d62286c 100644 --- a/src/programs/frameworks/elixir/elixir-wizard-agent.ts +++ b/src/programs/frameworks/elixir/elixir-wizard-agent.ts @@ -2,8 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { mixPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { mixPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; type ElixirContext = { diff --git a/src/programs/frameworks/fastapi/fastapi-wizard-agent.ts b/src/programs/frameworks/fastapi/fastapi-wizard-agent.ts index 22326fc49..5d022c3d3 100644 --- a/src/programs/frameworks/fastapi/fastapi-wizard-agent.ts +++ b/src/programs/frameworks/fastapi/fastapi-wizard-agent.ts @@ -1,8 +1,8 @@ /* FastAPI wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; -import { detectPythonPackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '../../framework-config'; +import { detectPythonPackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getFastAPIVersion, @@ -17,11 +17,16 @@ import * as path from 'node:path'; const EXTRA_IGNORE = ['**/env/**', '**/.env/**']; +type FastAPIContext = { + projectType?: FastAPIProjectType; + appFile?: string; +}; + /** * FastAPI framework configuration for the universal agent runner */ -export const FASTAPI_AGENT_CONFIG: FrameworkConfig = { +export const FASTAPI_AGENT_CONFIG: FrameworkConfig = { metadata: { name: 'FastAPI', integration: Integration.fastapi, @@ -32,6 +37,16 @@ export const FASTAPI_AGENT_CONFIG: FrameworkConfig = { const appFile = await findFastAPIAppFile(options); return { projectType, appFile }; }, + getDetectedFrameworkLabel: (context) => { + switch (context.projectType) { + case FastAPIProjectType.FULLSTACK: + return 'FastAPI fullstack with templates'; + case FastAPIProjectType.ROUTER: + return 'FastAPI with APIRouter'; + case FastAPIProjectType.STANDARD: + return 'FastAPI'; + } + }, }, detection: { diff --git a/src/programs/frameworks/fastapi/utils.ts b/src/programs/frameworks/fastapi/utils.ts index b208dd72f..8f5c08417 100644 --- a/src/programs/frameworks/fastapi/utils.ts +++ b/src/programs/frameworks/fastapi/utils.ts @@ -1,6 +1,5 @@ import { major, minVersion } from 'semver'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; -import { getUI } from '@ui'; import type { WizardRunOptions } from '@utils/types'; import * as path from 'node:path'; @@ -151,18 +150,15 @@ export async function getFastAPIProjectType( // Check for fullstack pattern (templates) if (await hasTemplates({ installDir })) { - getUI().setDetectedFramework('FastAPI fullstack with templates'); return FastAPIProjectType.FULLSTACK; } // Check for APIRouter (modular structure) if (await hasAPIRouter({ installDir })) { - getUI().setDetectedFramework('FastAPI with APIRouter'); return FastAPIProjectType.ROUTER; } // Default to standard FastAPI - getUI().setDetectedFramework('FastAPI'); return FastAPIProjectType.STANDARD; } diff --git a/src/programs/frameworks/flask/flask-wizard-agent.ts b/src/programs/frameworks/flask/flask-wizard-agent.ts index b781777fb..bb40e0564 100644 --- a/src/programs/frameworks/flask/flask-wizard-agent.ts +++ b/src/programs/frameworks/flask/flask-wizard-agent.ts @@ -1,8 +1,8 @@ /* Flask wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; -import { detectPythonPackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '../../framework-config'; +import { detectPythonPackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; @@ -33,6 +33,12 @@ export const FLASK_AGENT_CONFIG: FrameworkConfig = { const appFile = await findFlaskAppFile(options); return { projectType, appFile }; }, + getDetectedFrameworkLabel: (context) => + context.projectType === FlaskProjectType.STANDARD + ? 'Flask' + : context.projectType + ? getFlaskProjectTypeName(context.projectType) + : undefined, }, detection: { diff --git a/src/programs/frameworks/flask/utils.ts b/src/programs/frameworks/flask/utils.ts index 66eb46a1c..70442f5b1 100644 --- a/src/programs/frameworks/flask/utils.ts +++ b/src/programs/frameworks/flask/utils.ts @@ -1,5 +1,4 @@ import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; -import { getUI } from '@ui'; import type { WizardRunOptions } from '@utils/types'; import { createVersionBucket } from '@utils/semver'; import * as path from 'node:path'; @@ -237,30 +236,25 @@ export async function getFlaskProjectType( // Check for Flask-RESTX first (most specific - includes Swagger) if (await hasFlaskRESTX({ installDir })) { - getUI().setDetectedFramework('Flask-RESTX'); return FlaskProjectType.RESTX; } // Check for flask-smorest (OpenAPI-first) if (await hasFlaskSmorest({ installDir })) { - getUI().setDetectedFramework('flask-smorest'); return FlaskProjectType.SMOREST; } // Check for Flask-RESTful if (await hasFlaskRESTful({ installDir })) { - getUI().setDetectedFramework('Flask-RESTful'); return FlaskProjectType.RESTFUL; } // Check for Blueprints (large app structure) if (await hasBlueprints({ installDir })) { - getUI().setDetectedFramework('Flask with Blueprints'); return FlaskProjectType.BLUEPRINT; } // Default to standard Flask - getUI().setDetectedFramework('Flask'); return FlaskProjectType.STANDARD; } diff --git a/src/programs/frameworks/flutter/flutter-wizard-agent.ts b/src/programs/frameworks/flutter/flutter-wizard-agent.ts index 5a2e5e0a8..1e5a36eed 100644 --- a/src/programs/frameworks/flutter/flutter-wizard-agent.ts +++ b/src/programs/frameworks/flutter/flutter-wizard-agent.ts @@ -1,10 +1,10 @@ /* Flutter wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; +import type { FrameworkConfig } from '../../framework-config'; import { Integration } from '@shared/constants'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { pubPackageManager } from '@programs/detection/package-manager'; +import { pubPackageManager } from '../../detection/package-manager'; /** Platform subtrees `flutter create` scaffolds; each needs its own setup notes. */ const FLUTTER_PLATFORM_DIRS = [ diff --git a/src/programs/frameworks/go/go-wizard-agent.ts b/src/programs/frameworks/go/go-wizard-agent.ts index ae704da5b..a6b72157a 100644 --- a/src/programs/frameworks/go/go-wizard-agent.ts +++ b/src/programs/frameworks/go/go-wizard-agent.ts @@ -2,8 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { goModulesPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { goModulesPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; type GoContext = { diff --git a/src/programs/frameworks/java/java-wizard-agent.ts b/src/programs/frameworks/java/java-wizard-agent.ts index 8b27ab160..c8db1d038 100644 --- a/src/programs/frameworks/java/java-wizard-agent.ts +++ b/src/programs/frameworks/java/java-wizard-agent.ts @@ -3,8 +3,8 @@ import fg from 'fast-glob'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectJavaPackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectJavaPackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; type JavaContext = { diff --git a/src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts b/src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts index 4fc410be3..8de9f962a 100644 --- a/src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts +++ b/src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts @@ -1,8 +1,8 @@ /* Generic Node.js language wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '@programs/framework-config'; +import type { FrameworkConfig } from '../../framework-config'; import { Integration } from '@shared/constants'; import { tryGetPackageJson } from '@utils/setup-utils'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import { detectNodePackageManagers } from '../../detection/package-manager'; type JavaScriptNodeContext = Record; diff --git a/src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts b/src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts index 1a662796c..a080374f5 100644 --- a/src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts +++ b/src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts @@ -1,6 +1,6 @@ /* Generic JavaScript Web (client-side) wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; +import type { FrameworkConfig } from '../../framework-config'; import { Integration } from '@shared/constants'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -13,7 +13,7 @@ import { hasIndexHtml, type JavaScriptContext, } from './utils'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import { detectNodePackageManagers } from '../../detection/package-manager'; export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { metadata: { diff --git a/src/programs/frameworks/laravel/laravel-wizard-agent.ts b/src/programs/frameworks/laravel/laravel-wizard-agent.ts index cc0849617..654c5291d 100644 --- a/src/programs/frameworks/laravel/laravel-wizard-agent.ts +++ b/src/programs/frameworks/laravel/laravel-wizard-agent.ts @@ -1,7 +1,7 @@ /* Laravel wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { composerPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { composerPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import * as fs from 'node:fs'; @@ -43,6 +43,12 @@ export const LARAVEL_AGENT_CONFIG: FrameworkConfig = { laravelStructure, }; }, + getDetectedFrameworkLabel: (context) => + context.projectType === LaravelProjectType.STANDARD + ? 'Laravel' + : context.projectType + ? getLaravelProjectTypeName(context.projectType) + : undefined, }, detection: { diff --git a/src/programs/frameworks/laravel/utils.ts b/src/programs/frameworks/laravel/utils.ts index 08f9e6900..76c491249 100644 --- a/src/programs/frameworks/laravel/utils.ts +++ b/src/programs/frameworks/laravel/utils.ts @@ -1,5 +1,4 @@ import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; -import { getUI } from '@ui'; import type { WizardRunOptions } from '@utils/types'; import { createVersionBucket } from '@utils/semver'; import * as fs from 'node:fs'; @@ -166,16 +165,13 @@ export async function getLaravelProjectType( ): Promise { // Check for SPA/Reactive frameworks (important to detect - affects SDK needs) if (await hasInertia(options)) { - getUI().setDetectedFramework('Laravel with Inertia.js'); return LaravelProjectType.INERTIA; } if (await hasLivewire(options)) { - getUI().setDetectedFramework('Laravel with Livewire'); return LaravelProjectType.LIVEWIRE; } // Default to standard - getUI().setDetectedFramework('Laravel'); return LaravelProjectType.STANDARD; } diff --git a/src/programs/frameworks/nextjs/nextjs-wizard-agent.ts b/src/programs/frameworks/nextjs/nextjs-wizard-agent.ts index 83c180672..540c65840 100644 --- a/src/programs/frameworks/nextjs/nextjs-wizard-agent.ts +++ b/src/programs/frameworks/nextjs/nextjs-wizard-agent.ts @@ -1,7 +1,7 @@ /* Simplified Next.js wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, @@ -10,7 +10,6 @@ import { type PackageJson, } from '@utils/package-json'; import { tryGetPackageJson } from '@utils/setup-utils'; -import { getUI } from '@ui'; import { getNextJsRouter, getNextJsVersionBucket, @@ -31,15 +30,16 @@ export const NEXTJS_AGENT_CONFIG: FrameworkConfig = { gatherContext: async (options: WizardRunOptions) => { const router = await getNextJsRouter(options); if (router) { - const emoji = - router === NextJsRouter.APP_ROUTER ? '\u{1F4F1}' : '\u{1F4C3}'; - getUI().setDetectedFramework( - `Next.js ${getNextJsRouterName(router)} ${emoji}`, - ); return { router }; } return {}; }, + getDetectedFrameworkLabel: (context) => { + if (!context.router) return undefined; + const emoji = + context.router === NextJsRouter.APP_ROUTER ? '\u{1F4F1}' : '\u{1F4C3}'; + return `Next.js ${getNextJsRouterName(context.router)} ${emoji}`; + }, setup: { questions: [ { diff --git a/src/programs/frameworks/nuxt/nuxt-wizard-agent.ts b/src/programs/frameworks/nuxt/nuxt-wizard-agent.ts index 7e197aca6..e844be692 100644 --- a/src/programs/frameworks/nuxt/nuxt-wizard-agent.ts +++ b/src/programs/frameworks/nuxt/nuxt-wizard-agent.ts @@ -1,7 +1,7 @@ /* Nuxt wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/programs/frameworks/python/python-wizard-agent.ts b/src/programs/frameworks/python/python-wizard-agent.ts index 63fcf83aa..8e9b45024 100644 --- a/src/programs/frameworks/python/python-wizard-agent.ts +++ b/src/programs/frameworks/python/python-wizard-agent.ts @@ -1,8 +1,8 @@ /* Generic Python language wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; -import { detectPythonPackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '../../framework-config'; +import { detectPythonPackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import fg from 'fast-glob'; import * as fs from 'node:fs'; diff --git a/src/programs/frameworks/rails/rails-wizard-agent.ts b/src/programs/frameworks/rails/rails-wizard-agent.ts index 08c8c39a6..04c527155 100644 --- a/src/programs/frameworks/rails/rails-wizard-agent.ts +++ b/src/programs/frameworks/rails/rails-wizard-agent.ts @@ -1,7 +1,7 @@ /* Ruby on Rails wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { bundlerPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { bundlerPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getRailsVersion, @@ -29,6 +29,12 @@ export const RAILS_AGENT_CONFIG: FrameworkConfig = { const initializersDir = findInitializersDir(options); return Promise.resolve({ projectType, initializersDir }); }, + getDetectedFrameworkLabel: (context) => + context.projectType === RailsProjectType.API + ? 'Rails API-only' + : context.projectType === RailsProjectType.STANDARD + ? 'Rails' + : undefined, }, detection: { diff --git a/src/programs/frameworks/rails/utils.ts b/src/programs/frameworks/rails/utils.ts index ab7993e4d..5b7820d60 100644 --- a/src/programs/frameworks/rails/utils.ts +++ b/src/programs/frameworks/rails/utils.ts @@ -1,5 +1,4 @@ import fg from 'fast-glob'; -import { getUI } from '@ui'; import type { WizardRunOptions } from '@utils/types'; import { createVersionBucket } from '@utils/semver'; import * as fs from 'node:fs'; @@ -96,7 +95,6 @@ export function getRailsProjectType( try { const content = fs.readFileSync(appConfigPath, 'utf-8'); if (content.includes('config.api_only = true')) { - getUI().setDetectedFramework('Rails API-only'); return RailsProjectType.API; } } catch { @@ -104,7 +102,6 @@ export function getRailsProjectType( } } - getUI().setDetectedFramework('Rails'); return RailsProjectType.STANDARD; } diff --git a/src/programs/frameworks/react-native/react-native-wizard-agent.ts b/src/programs/frameworks/react-native/react-native-wizard-agent.ts index 2ba1b8f85..842fe945c 100644 --- a/src/programs/frameworks/react-native/react-native-wizard-agent.ts +++ b/src/programs/frameworks/react-native/react-native-wizard-agent.ts @@ -1,7 +1,7 @@ /* React Native wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, @@ -30,6 +30,10 @@ export const REACT_NATIVE_AGENT_CONFIG: FrameworkConfig = { const variant = await detectReactNativeVariant(options); return { variant }; }, + getDetectedFrameworkLabel: (context) => + context.variant + ? `${getReactNativeVariantName(context.variant)} 📱` + : undefined, }, detection: { diff --git a/src/programs/frameworks/react-native/utils.ts b/src/programs/frameworks/react-native/utils.ts index a3601217f..e8617ca71 100644 --- a/src/programs/frameworks/react-native/utils.ts +++ b/src/programs/frameworks/react-native/utils.ts @@ -1,7 +1,6 @@ import { createVersionBucket } from '@utils/semver'; import { tryGetPackageJson } from '@utils/setup-utils'; import { hasDeclaredDependency } from '@utils/package-json'; -import { getUI } from '@ui'; import type { WizardRunOptions } from '@utils/types'; export const getReactNativeVersionBucket = createVersionBucket(); @@ -21,14 +20,8 @@ export async function detectReactNativeVariant( const packageJson = await tryGetPackageJson(options); if (packageJson && hasDeclaredDependency('expo', packageJson)) { - getUI().setDetectedFramework( - `${getReactNativeVariantName(ReactNativeVariant.EXPO)} 📱`, - ); return ReactNativeVariant.EXPO; } - getUI().setDetectedFramework( - `${getReactNativeVariantName(ReactNativeVariant.REACT_NATIVE)} 📱`, - ); return ReactNativeVariant.REACT_NATIVE; } diff --git a/src/programs/frameworks/react-router/react-router-wizard-agent.ts b/src/programs/frameworks/react-router/react-router-wizard-agent.ts index b4dd9b38a..8f8383020 100644 --- a/src/programs/frameworks/react-router/react-router-wizard-agent.ts +++ b/src/programs/frameworks/react-router/react-router-wizard-agent.ts @@ -1,7 +1,7 @@ /* React Router wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, @@ -10,7 +10,6 @@ import { type PackageJson, } from '@utils/package-json'; import { tryGetPackageJson } from '@utils/setup-utils'; -import { getUI } from '@ui'; import { getReactRouterMode, getReactRouterModeName, @@ -31,13 +30,14 @@ export const REACT_ROUTER_AGENT_CONFIG: FrameworkConfig = { gatherContext: async (options: WizardRunOptions) => { const routerMode = await getReactRouterMode(options); if (routerMode) { - getUI().setDetectedFramework( - `React Router ${getReactRouterModeName(routerMode)}`, - ); return { routerMode }; } return {}; }, + getDetectedFrameworkLabel: (context) => + context.routerMode + ? `React Router ${getReactRouterModeName(context.routerMode)}` + : undefined, }, detection: { diff --git a/src/programs/frameworks/registry.ts b/src/programs/frameworks/registry.ts index 4c2a79a70..945024311 100644 --- a/src/programs/frameworks/registry.ts +++ b/src/programs/frameworks/registry.ts @@ -1,32 +1,32 @@ -import type { FrameworkConfig } from '@programs/framework-config'; +import type { FrameworkConfig } from '../framework-config'; import { Integration } from '@shared/constants'; -import { NEXTJS_AGENT_CONFIG } from '@programs/frameworks/nextjs/nextjs-wizard-agent'; -import { NUXT_AGENT_CONFIG } from '@programs/frameworks/nuxt/nuxt-wizard-agent'; -import { VUE_AGENT_CONFIG } from '@programs/frameworks/vue/vue-wizard-agent'; -import { REACT_ROUTER_AGENT_CONFIG } from '@programs/frameworks/react-router/react-router-wizard-agent'; -import { TANSTACK_ROUTER_AGENT_CONFIG } from '@programs/frameworks/tanstack-router/tanstack-router-wizard-agent'; -import { TANSTACK_START_AGENT_CONFIG } from '@programs/frameworks/tanstack-start/tanstack-start-wizard-agent'; -import { REACT_NATIVE_AGENT_CONFIG } from '@programs/frameworks/react-native/react-native-wizard-agent'; -import { ANGULAR_AGENT_CONFIG } from '@programs/frameworks/angular/angular-wizard-agent'; -import { ASTRO_AGENT_CONFIG } from '@programs/frameworks/astro/astro-wizard-agent'; -import { DJANGO_AGENT_CONFIG } from '@programs/frameworks/django/django-wizard-agent'; -import { FLASK_AGENT_CONFIG } from '@programs/frameworks/flask/flask-wizard-agent'; -import { FASTAPI_AGENT_CONFIG } from '@programs/frameworks/fastapi/fastapi-wizard-agent'; -import { LARAVEL_AGENT_CONFIG } from '@programs/frameworks/laravel/laravel-wizard-agent'; -import { SVELTEKIT_AGENT_CONFIG } from '@programs/frameworks/svelte/svelte-wizard-agent'; -import { FLUTTER_AGENT_CONFIG } from '@programs/frameworks/flutter/flutter-wizard-agent'; -import { SWIFT_AGENT_CONFIG } from '@programs/frameworks/swift/swift-wizard-agent'; -import { KMP_AGENT_CONFIG } from '@programs/frameworks/kmp/kmp-wizard-agent'; -import { ANDROID_AGENT_CONFIG } from '@programs/frameworks/android/android-wizard-agent'; -import { RAILS_AGENT_CONFIG } from '@programs/frameworks/rails/rails-wizard-agent'; -import { ELIXIR_AGENT_CONFIG } from '@programs/frameworks/elixir/elixir-wizard-agent'; -import { GO_AGENT_CONFIG } from '@programs/frameworks/go/go-wizard-agent'; -import { JAVA_AGENT_CONFIG } from '@programs/frameworks/java/java-wizard-agent'; -import { RUST_AGENT_CONFIG } from '@programs/frameworks/rust/rust-wizard-agent'; -import { PYTHON_AGENT_CONFIG } from '@programs/frameworks/python/python-wizard-agent'; -import { RUBY_AGENT_CONFIG } from '@programs/frameworks/ruby/ruby-wizard-agent'; -import { JAVASCRIPT_NODE_AGENT_CONFIG } from '@programs/frameworks/javascript-node/javascript-node-wizard-agent'; -import { JAVASCRIPT_WEB_AGENT_CONFIG } from '@programs/frameworks/javascript-web/javascript-web-wizard-agent'; +import { NEXTJS_AGENT_CONFIG } from './nextjs/nextjs-wizard-agent'; +import { NUXT_AGENT_CONFIG } from './nuxt/nuxt-wizard-agent'; +import { VUE_AGENT_CONFIG } from './vue/vue-wizard-agent'; +import { REACT_ROUTER_AGENT_CONFIG } from './react-router/react-router-wizard-agent'; +import { TANSTACK_ROUTER_AGENT_CONFIG } from './tanstack-router/tanstack-router-wizard-agent'; +import { TANSTACK_START_AGENT_CONFIG } from './tanstack-start/tanstack-start-wizard-agent'; +import { REACT_NATIVE_AGENT_CONFIG } from './react-native/react-native-wizard-agent'; +import { ANGULAR_AGENT_CONFIG } from './angular/angular-wizard-agent'; +import { ASTRO_AGENT_CONFIG } from './astro/astro-wizard-agent'; +import { DJANGO_AGENT_CONFIG } from './django/django-wizard-agent'; +import { FLASK_AGENT_CONFIG } from './flask/flask-wizard-agent'; +import { FASTAPI_AGENT_CONFIG } from './fastapi/fastapi-wizard-agent'; +import { LARAVEL_AGENT_CONFIG } from './laravel/laravel-wizard-agent'; +import { SVELTEKIT_AGENT_CONFIG } from './svelte/svelte-wizard-agent'; +import { FLUTTER_AGENT_CONFIG } from './flutter/flutter-wizard-agent'; +import { SWIFT_AGENT_CONFIG } from './swift/swift-wizard-agent'; +import { KMP_AGENT_CONFIG } from './kmp/kmp-wizard-agent'; +import { ANDROID_AGENT_CONFIG } from './android/android-wizard-agent'; +import { RAILS_AGENT_CONFIG } from './rails/rails-wizard-agent'; +import { ELIXIR_AGENT_CONFIG } from './elixir/elixir-wizard-agent'; +import { GO_AGENT_CONFIG } from './go/go-wizard-agent'; +import { JAVA_AGENT_CONFIG } from './java/java-wizard-agent'; +import { RUST_AGENT_CONFIG } from './rust/rust-wizard-agent'; +import { PYTHON_AGENT_CONFIG } from './python/python-wizard-agent'; +import { RUBY_AGENT_CONFIG } from './ruby/ruby-wizard-agent'; +import { JAVASCRIPT_NODE_AGENT_CONFIG } from './javascript-node/javascript-node-wizard-agent'; +import { JAVASCRIPT_WEB_AGENT_CONFIG } from './javascript-web/javascript-web-wizard-agent'; export const FRAMEWORK_REGISTRY: Record = { [Integration.nextjs]: NEXTJS_AGENT_CONFIG, diff --git a/src/programs/frameworks/ruby/ruby-wizard-agent.ts b/src/programs/frameworks/ruby/ruby-wizard-agent.ts index c2907a536..630e54236 100644 --- a/src/programs/frameworks/ruby/ruby-wizard-agent.ts +++ b/src/programs/frameworks/ruby/ruby-wizard-agent.ts @@ -1,7 +1,7 @@ /* Generic Ruby language wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { bundlerPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { bundlerPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getRubyVersion, diff --git a/src/programs/frameworks/rust/rust-wizard-agent.ts b/src/programs/frameworks/rust/rust-wizard-agent.ts index a1dd2711d..1a99955c9 100644 --- a/src/programs/frameworks/rust/rust-wizard-agent.ts +++ b/src/programs/frameworks/rust/rust-wizard-agent.ts @@ -2,8 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { cargoPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { cargoPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; type RustContext = { diff --git a/src/programs/frameworks/svelte/svelte-wizard-agent.ts b/src/programs/frameworks/svelte/svelte-wizard-agent.ts index 1c5088c3c..ae9819e49 100644 --- a/src/programs/frameworks/svelte/svelte-wizard-agent.ts +++ b/src/programs/frameworks/svelte/svelte-wizard-agent.ts @@ -1,6 +1,6 @@ /* SvelteKit wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/programs/frameworks/swift/swift-wizard-agent.ts b/src/programs/frameworks/swift/swift-wizard-agent.ts index 58dd25782..1d97df1c7 100644 --- a/src/programs/frameworks/swift/swift-wizard-agent.ts +++ b/src/programs/frameworks/swift/swift-wizard-agent.ts @@ -1,7 +1,7 @@ /* Swift wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { swiftPackageManager } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { swiftPackageManager } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import fg from 'fast-glob'; diff --git a/src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts b/src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts index d93e337bf..f489dd91f 100644 --- a/src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts +++ b/src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts @@ -1,7 +1,7 @@ /* TanStack Router wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, @@ -10,7 +10,6 @@ import { type PackageJson, } from '@utils/package-json'; import { tryGetPackageJson } from '@utils/setup-utils'; -import { getUI } from '@ui'; import { getTanStackRouterMode, getTanStackRouterModeName, @@ -31,13 +30,14 @@ export const TANSTACK_ROUTER_AGENT_CONFIG: FrameworkConfig { const routerMode = await getTanStackRouterMode(options); if (routerMode) { - getUI().setDetectedFramework( - `TanStack Router ${getTanStackRouterModeName(routerMode)}`, - ); return { routerMode }; } return {}; }, + getDetectedFrameworkLabel: (context) => + context.routerMode + ? `TanStack Router ${getTanStackRouterModeName(context.routerMode)}` + : undefined, }, detection: { diff --git a/src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts b/src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts index fcb52a0d6..2c7c4316b 100644 --- a/src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts +++ b/src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts @@ -1,7 +1,7 @@ /* TanStack Start wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/programs/frameworks/vue/vue-wizard-agent.ts b/src/programs/frameworks/vue/vue-wizard-agent.ts index 0f2ae5cee..629a59f95 100644 --- a/src/programs/frameworks/vue/vue-wizard-agent.ts +++ b/src/programs/frameworks/vue/vue-wizard-agent.ts @@ -1,6 +1,6 @@ /* Vue wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '@programs/framework-config'; -import { detectNodePackageManagers } from '@programs/detection/package-manager'; +import type { FrameworkConfig } from '../../framework-config'; +import { detectNodePackageManagers } from '../../detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/programs/mcp-analytics/index.ts b/src/programs/mcp-analytics/index.ts index 49d913876..3604dfb41 100644 --- a/src/programs/mcp-analytics/index.ts +++ b/src/programs/mcp-analytics/index.ts @@ -1,6 +1,6 @@ import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; -import { createSkillProgram } from '@programs/shared/skill-program'; +import { createSkillProgram } from '../shared/skill-program'; const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; @@ -54,7 +54,7 @@ export const MCP_ANALYTICS_ABORT_CASES: AbortCase[] = [ * 'mcp-analytics'` from context-mill — a deliberate breaking change, done then, * not pre-emptively. */ -export const mcpAnalyticsConfig = createSkillProgram({ +export const config = createSkillProgram({ skillId: 'mcp-analytics', command: 'mcp-analytics', id: 'mcp-analytics', diff --git a/src/programs/metrics/index.ts b/src/programs/metrics/index.ts index 78d9d3ce0..bb83feeb6 100644 --- a/src/programs/metrics/index.ts +++ b/src/programs/metrics/index.ts @@ -1,10 +1,5 @@ -import type { ProgramConfig, ProgramStep } from '@programs/program-step'; -import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; -import { getContentBlocks } from '@tui/programs/shared/skill-deck'; - -const METRICS_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => - step.id === 'intro' ? { ...step, screenId: 'metrics-intro' } : step, -); +import { Harness, Sequence, DEFAULT_AGENT_MODEL } from '@shared/constants'; +import type { ProgramConfig } from '../program-step'; const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; @@ -18,18 +13,24 @@ const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; * `customPrompt` both pick from the menu and install it. Stays flat while a * single "add metrics to a project" flow is the only action. */ -export const metricsConfig: ProgramConfig = { +export const config: ProgramConfig = { command: 'metrics', description: 'Add PostHog application metrics to your project', id: 'metrics', + // Orchestrator on pi. 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). + binding: { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, // Orchestrator flow (context-mill `context/agents/metrics`): the seed queues // verify-sdk → instrument-metrics → report; the tasks install the matching // platform variant themselves. Explicit so renaming the program can't // silently detach the flow. agentFlow: 'metrics', - steps: METRICS_STEPS, reportFile: METRICS_REPORT_FILE, - getContentBlocks, run: { integrationLabel: 'metrics', // No `skillId`: the agent must load the menu and install the right diff --git a/src/programs/migration/index.ts b/src/programs/migration/index.ts index 5f893f5c3..f8a39c63a 100644 --- a/src/programs/migration/index.ts +++ b/src/programs/migration/index.ts @@ -1,8 +1,6 @@ -import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramConfig } from '../program-step'; import type { AbortCase } from '@agent/types'; import { WIZARD_TOOL_NAMES } from '@agent'; -import { MIGRATION_PROGRAM } from '../../tui/programs/migration/flow.js'; -import { getContentBlocks } from '../../tui/programs/migration/deck/index.js'; const MIGRATION_REPORT_FILE = 'migration-report.md'; @@ -21,17 +19,15 @@ const MIGRATION_ABORT_CASES: AbortCase[] = [ // Default skill id when nothing else picks one. The `wizard migrate ` // subcommands override this via skillCommandFactory using each manifest // entry's skillId, so this default only kicks in for legacy callers (e.g. -// programmatic uses of migrationConfig directly). +// programmatic uses of this config directly). const DEFAULT_MIGRATE_SKILL_ID = 'migrate-statsig'; -export const migrationConfig: ProgramConfig = { +export const config: ProgramConfig = { command: 'migrate', description: 'Migrate to PostHog from another analytics provider', id: 'migration', skillId: DEFAULT_MIGRATE_SKILL_ID, - steps: MIGRATION_PROGRAM, reportFile: MIGRATION_REPORT_FILE, - getContentBlocks, allowedTools: ['Agent'], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], run: { @@ -53,5 +49,3 @@ export const migrationConfig: ProgramConfig = { }, requires: ['posthog-integration'], }; - -export { MIGRATION_PROGRAM } from '../../tui/programs/migration/flow.js'; diff --git a/src/programs/posthog-integration/constants.ts b/src/programs/posthog-integration/constants.ts index 957c41a14..013056a78 100644 --- a/src/programs/posthog-integration/constants.ts +++ b/src/programs/posthog-integration/constants.ts @@ -1,6 +1,6 @@ /** * Leaf-level constants for the posthog-integration program. The event plan's - * name lives in `@lib/constants` so infrastructure (the scanner's allowlist) + * name lives in `@shared/constants` so infrastructure (the scanner's allowlist) * can name it without importing this program. */ diff --git a/src/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts index df0999cf3..1bab5eaaf 100644 --- a/src/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -1,40 +1,38 @@ -import type { ProgramConfig, ProgramStep } from '@programs/program-step'; -import { runProgramAgent } from '@programs/run-agent-legacy'; -import type { ProgramRun } from '@programs/program-run'; -import { AgentSignals, shouldDisableAsk, WIZARD_TOOL_NAMES } from '@agent'; -import type { WizardSession } from '@programs/session/wizard-session'; -import { - mayReportScanResults, - OutroKind, - RunPhase, -} from '@programs/session/wizard-session'; +import { detectPostHogIntegration } from '../detection/integration.js'; +import type { ProgramConfig } from '../program-step'; +import type { ProgramRun } from '../program-run'; +import { AgentSignals, WIZARD_TOOL_NAMES } from '@agent'; +import { shouldDisableAsk } from '@shared/ask-policy'; +import type { ProgramSession } from '../program-session'; +import { mayReportScanResults } from '@shared/run-state'; +import { OutroKind } from '@shared/outro'; import { DEFAULT_PACKAGE_INSTALLATION, SPINNER_MESSAGE, -} from '@programs/framework-config'; +} from '../framework-config'; import { tryGetPackageJson, isUsingTypeScript } from '@utils/setup-utils'; import { analytics } from '@utils/analytics'; -import { - detectFramework, - gatherFrameworkContext, -} from '@programs/detection/index'; -import { scopeInstallDirToProject } from '@programs/detection/project-scope'; -import type { CiRunnerContext, RunnerContext } from '@programs/runner-context'; -import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; -import { wizardAbort } from '@host/wizard-abort'; +import { detectFramework } from '../detection/framework'; +import { gatherFrameworkContext } from '../detection/context'; +import { noteDetectedFramework } from '../detection/detected-framework'; +import { scopeInstallDirToProject } from '../detection/project-scope'; +import type { CiRunnerContext, RunnerContext } from '../runner-context'; +import { FRAMEWORK_REGISTRY } from '../frameworks/registry'; +import { ProgramAbort } from '../program-abort'; import { ErrorCodes } from '@shared/errors'; import { + SETUP_REPORT_FILE, WIZARD_DEFAULT_AIO_LOGS_FLAG_KEY, WIZARD_INTERACTION_EVENT_NAME, } from '@shared/constants'; import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; import type { HostResolution } from '@shared/host-resolution'; -import { getDetectedWarehouseSources } from '@programs/warehouse-source/detect'; -import { POSTHOG_INTEGRATION_PROGRAM } from '../../tui/programs/posthog-integration/flow.js'; -import { getContentBlocks } from '../../tui/programs/posthog-integration/deck/index.js'; +import { getDetectedWarehouseSources } from '../warehouse-sources/detect'; import { buildCodingAgentPrompt } from './handoff.js'; import { EVENT_PLAN_FILE } from './constants.js'; +import { WAREHOUSE_SOURCE_SCOPE_ADDITIONS } from '../oauth/program-scopes'; +import { CONNECT_SLACK_SCOPE_ADDITIONS } from '@shared/oauth-scopes'; const DASHBOARD_DEEP_LINK_KEY = 'dashboardDeepLink'; @@ -45,7 +43,7 @@ const WAREHOUSE_SOURCES_DOCS_URL = const WAREHOUSE_SEED_TASK_TYPE = 'warehouse'; function resolveContinueUrl( - sess: WizardSession, + sess: ProgramSession, host: HostResolution, deepLink: unknown, ): string | undefined { @@ -102,7 +100,7 @@ function warehouseSourceUrl( * * A pointer at the app, not an inline flow. Connecting a source needs * interactive credential collection, and chaining that as a second agent run - * before the outro would let any of its terminal failure paths `process.exit()` + * before the outro would let any of its terminal failure paths end the run * — costing the user the success outro and the post-outro MCP / Slack steps on * a run where PostHog installed fine. * @@ -121,7 +119,7 @@ function warehouseSourceUrl( * past that is still unconnected and still belongs here. */ function buildWarehouseNextSteps( - sess: WizardSession, + sess: ProgramSession, host: HostResolution, projectId: number | string, completedSeededTypes: readonly string[], @@ -156,7 +154,7 @@ function buildWarehouseNextSteps( * because it is a note in a report: the outro `nextSteps` bullet carries the * same information deterministically, so nothing is lost if the agent drops it. */ -function warehouseReportInstruction(sess: WizardSession): string { +function warehouseReportInstruction(sess: ProgramSession): string { const sources = getDetectedWarehouseSources(sess); if (sources.length === 0) return ''; @@ -239,21 +237,27 @@ const warehouseSeedTasks: NonNullable = (sess) => { ]; }; -export const SETUP_REPORT_FILE = 'posthog-setup-report.md'; export { EVENT_PLAN_FILE } from './constants.js'; -export const posthogIntegrationConfig: ProgramConfig = { +export const config: ProgramConfig = { description: 'Set up PostHog SDK integration', id: 'posthog-integration', agentFlow: 'integration-v2', eventPlanFile: EVENT_PLAN_FILE, - steps: POSTHOG_INTEGRATION_PROGRAM, - getContentBlocks, + onReady: (ctx) => detectPostHogIntegration(ctx), // Basic integration runs without structured user input; drop wizard_ask // so the model can't pop modal prompts mid-run. The runner forwards this // list to the general-purpose subagent as well, so dispatched subagents // can't reach around the parent and ask either. disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], + // The integration run carries the Slack outro step, and — when detection + // finds data sources — the orchestrator's warehouse task, which creates + // sources through `external-data-sources-create`. Without the warehouse pair + // that call 403s on a token the user already granted. + oauthScopeAdditions: [ + ...CONNECT_SLACK_SCOPE_ADDITIONS, + ...WAREHOUSE_SOURCE_SCOPE_ADDITIONS, + ], seedTasks: warehouseSeedTasks, @@ -267,18 +271,17 @@ export const posthogIntegrationConfig: ProgramConfig = { // CI-mode prerequisite work: the headless equivalent of the detect step's // onReady hook. Auto-detect the framework, then gather context. ciPreRun: async ( - session: WizardSession, + session: ProgramSession, runner: CiRunnerContext, ): Promise => { await scopeInstallDirToProject(session, runner); const integration = await detectFramework(session.installDir); if (!integration) { - await wizardAbort({ + throw new ProgramAbort({ code: ErrorCodes.DetectNoFramework, message: 'Could not auto-detect your framework for this project.', }); - return; } session.integration = integration; analytics.setTag('integration', integration); @@ -299,10 +302,11 @@ export const posthogIntegrationConfig: ProgramConfig = { session.frameworkContext[key] = value; } } + noteDetectedFramework(session, frameworkConfig, context, runner.log); }, run: async ( - session: WizardSession, + session: ProgramSession, runner: RunnerContext, ): Promise => { const config = session.frameworkConfig!; @@ -443,13 +447,14 @@ ${warehouseReportInstruction(session)} ); if (config.environment.uploadToHosting) { const { uploadEnvironmentVariablesStep } = await import( - '@steps/index' + './upload-environment-variables/upload-step' ); const uploadedEnvVars = await uploadEnvironmentVariablesStep( envVars, { integration: config.metadata.integration, - session: sess, + installDir: sess.installDir, + runner, }, ); if (uploadedEnvVars.length > 0) { @@ -529,25 +534,3 @@ ${warehouseReportInstruction(session)} }; }, }; - -export { POSTHOG_INTEGRATION_PROGRAM } from '../../tui/programs/posthog-integration/flow.js'; - -/** - * Self-contained run step that runs the integration agent. Other programs - * import this and splice it into their own step list to compose the - * integration's work as one of their run steps — self-driving sets up PostHog - * this way before its own run. The host program supplies `show`/`onRunPrep`/ - * `targetDir`; this carries the run. - */ -export const integrationRunStep: ProgramStep = { - id: 'run', - label: 'Integration', - screenId: 'run', - // composed: runs inside the host program (self-driving), so skip the - // integration's terminal outro + analytics shutdown of the shared client. - run: (session) => - runProgramAgent(posthogIntegrationConfig, session, { composed: true }), - isComplete: (session) => - session.runPhase === RunPhase.Completed || - session.runPhase === RunPhase.Error, -}; diff --git a/src/programs/posthog-integration/upload-environment-variables/EnvironmentProvider.ts b/src/programs/posthog-integration/upload-environment-variables/EnvironmentProvider.ts index e71631acf..844b4f2a3 100644 --- a/src/programs/posthog-integration/upload-environment-variables/EnvironmentProvider.ts +++ b/src/programs/posthog-integration/upload-environment-variables/EnvironmentProvider.ts @@ -1,9 +1,17 @@ +import type { RunnerContext } from '../../runner-context'; + +export type EnvironmentProviderOptions = { + installDir: string; + /** Where the upload reports progress: the running program's runner. */ + runner: Pick; +}; + export abstract class EnvironmentProvider { - protected options: { installDir: string }; + protected options: EnvironmentProviderOptions; name: string; - constructor(options: { installDir: string }) { + constructor(options: EnvironmentProviderOptions) { this.options = options; } diff --git a/src/programs/posthog-integration/upload-environment-variables/providers/vercel.ts b/src/programs/posthog-integration/upload-environment-variables/providers/vercel.ts index 5fd3ba413..0a4d87bd6 100644 --- a/src/programs/posthog-integration/upload-environment-variables/providers/vercel.ts +++ b/src/programs/posthog-integration/upload-environment-variables/providers/vercel.ts @@ -1,15 +1,17 @@ import { execSync, spawn, spawnSync } from 'child_process'; -import { EnvironmentProvider } from '@programs/posthog-integration/upload-environment-variables/EnvironmentProvider'; +import { + EnvironmentProvider, + type EnvironmentProviderOptions, +} from '../EnvironmentProvider'; import * as fs from 'fs'; import * as path from 'path'; -import { getUI } from '@ui'; import { analytics } from '@utils/analytics'; export class VercelEnvironmentProvider extends EnvironmentProvider { name = 'Vercel'; environments = ['production', 'preview', 'development']; - constructor(options: { installDir: string }) { + constructor(options: EnvironmentProviderOptions) { super(options); } @@ -126,7 +128,7 @@ export class VercelEnvironmentProvider extends EnvironmentProvider { const results: Record = {}; for (const [key, value] of Object.entries(vars)) { - const spinner = getUI().spinner(); + const spinner = this.options.runner.spinner(); spinner.start(`Uploading ${key} to ${this.name}...`); await Promise.all( diff --git a/src/programs/posthog-integration/upload-environment-variables/upload-step.ts b/src/programs/posthog-integration/upload-environment-variables/upload-step.ts index a169ba2ea..adc3b6c69 100644 --- a/src/programs/posthog-integration/upload-environment-variables/upload-step.ts +++ b/src/programs/posthog-integration/upload-environment-variables/upload-step.ts @@ -1,8 +1,7 @@ import type { Integration } from '@shared/constants'; import { withProgress } from '@utils/telemetry'; import { analytics } from '@utils/analytics'; -import { getUI } from '@ui'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { RunnerContext } from '../../runner-context'; import { EnvironmentProvider } from './EnvironmentProvider'; import { VercelEnvironmentProvider } from './providers/vercel'; @@ -10,14 +9,16 @@ export const uploadEnvironmentVariablesStep = async ( envVars: Record, { integration, - session, + installDir, + runner, }: { integration: Integration; - session: WizardSession; + installDir: string; + runner: Pick; }, ): Promise => { const providers: EnvironmentProvider[] = [ - new VercelEnvironmentProvider({ installDir: session.installDir }), + new VercelEnvironmentProvider({ installDir, runner }), ]; let provider: EnvironmentProvider | null = null; @@ -38,7 +39,7 @@ export const uploadEnvironmentVariablesStep = async ( } // Auto-accept — the agent already wrote env vars via MCP tools - getUI().log.info(`Uploading environment variables to ${provider.name}...`); + runner.log.info(`Uploading environment variables to ${provider.name}...`); const results = await withProgress( 'uploading environment variables', diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index c9f3e798e..40ea6ab37 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -1,72 +1,36 @@ import type { AbortCase } from '@agent/types'; -import { Integration } from '@shared/constants'; import { - detectFramework, - gatherFrameworkContext, -} from '@programs/detection/index'; -import { scopeInstallDirToProject } from '@programs/detection/project-scope'; -import type { CiRunnerContext } from '@programs/runner-context'; -import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; -import { createSkillProgram } from '@programs/shared/skill-program'; -import { AGENT_SKILL_STEPS } from '@tui/programs/shared/skill-flow'; -import { detectPostHogIntegration } from '@programs/detection/integration'; -import type { - ProgramConfig, - ProgramReadyContext, - ProgramStep, -} from '@programs/program-step'; -import type { WizardSession } from '@programs/session/wizard-session'; + Harness, + Integration, + Sequence, + DEFAULT_AGENT_MODEL, + REPLAY_VISION_SUPPORTED, +} from '@shared/constants'; +import { detectFramework } from '../detection/framework'; +import { gatherFrameworkContext } from '../detection/context'; +import { noteDetectedFramework } from '../detection/detected-framework'; +import { scopeInstallDirToProject } from '../detection/project-scope'; +import type { CiRunnerContext } from '../runner-context'; +import { FRAMEWORK_REGISTRY } from '../frameworks/registry'; +import { createSkillProgram } from '../shared/skill-program'; +import { detectPostHogIntegration } from '../detection/integration'; +import type { ProgramConfig, ProgramReadyContext } from '../program-step'; +import type { ProgramSession } from '../program-session'; import { analytics } from '@utils/analytics'; -import { wizardAbort } from '@host/wizard-abort'; +import { ProgramAbort } from '../program-abort'; import { ErrorCodes } from '@shared/errors'; +import { REPLAY_VISION_SCOPE_ADDITIONS } from './scopes.js'; const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; -/** - * The platforms session replay can actually record on. Replay vision watches - * recordings, so a platform with no recordings has nothing to set up — the - * run must stop before any work, not after a pointless agent run. - * - * Web frameworks record through posthog-js (server-rendered frameworks - * included — they serve pages), and the mobile SDKs with replay support are - * React Native, Android, iOS, and Flutter. Excluded: pure backend targets - * (`javascript_node`, `python`, `ruby`) and KMP, which has no replay support - * yet. - */ -export const REPLAY_VISION_SUPPORTED: ReadonlySet = new Set([ - Integration.nextjs, - Integration.nuxt, - Integration.vue, - Integration.reactRouter, - Integration.tanstackStart, - Integration.tanstackRouter, - Integration.angular, - Integration.astro, - Integration.sveltekit, - Integration.javascript_web, - Integration.django, - Integration.flask, - Integration.fastapi, - Integration.laravel, - Integration.rails, - Integration.reactNative, - Integration.android, - Integration.swift, - Integration.flutter, -]); - -async function abortUnsupportedPlatform( - integration: Integration, -): Promise { +function abortUnsupportedPlatform(integration: Integration): never { const name = FRAMEWORK_REGISTRY[integration]?.metadata.name ?? integration; // This is a clean, intentional exit, not a crash. Count it with a normal - // event keyed on the platform so aborts roll up into one series. Do not hand - // `wizardAbort` an `error` — that forwards to captureException and mints a - // new error-tracking issue per install location and per platform. + // event keyed on the platform so aborts roll up into one series. analytics.wizardCapture('replay-vision unsupported platform', { integration, }); - await wizardAbort({ + throw new ProgramAbort({ code: ErrorCodes.DetectUnsupportedPlatform, message: `Session replay isn't available for ${name} projects, and Replay ` + @@ -103,22 +67,19 @@ export const REPLAY_VISION_ABORT_CASES: AbortCase[] = [ * preflight. Without this step the session would still carry the program's * own skill id and preflight would abort. */ -const DETECT_STEP: ProgramStep = { - id: 'detect', - label: 'Detecting framework', - // The platform gate runs on a direct detectFramework call BEFORE the full - // detect writes to the store: store setters replace the session with a - // shallow copy, so `ctx.session` read after detectPostHogIntegration would - // be the stale pre-copy object (see the warning in detect.ts). - onReady: async (ctx: ProgramReadyContext) => { - const integration = await detectFramework(ctx.session.installDir); - if (integration && !REPLAY_VISION_SUPPORTED.has(integration)) { - await abortUnsupportedPlatform(integration); - return; - } - await detectPostHogIntegration(ctx); - }, -}; +// The platform gate runs on a direct detectFramework call BEFORE the full +// detect writes to the store: store setters replace the session with a +// shallow copy, so `ctx.session` read after detectPostHogIntegration would +// be the stale pre-copy object (see the warning in detect.ts). +async function detectReplayVisionProject( + ctx: ProgramReadyContext, +): Promise { + const integration = await detectFramework(ctx.session.installDir); + if (integration && !REPLAY_VISION_SUPPORTED.has(integration)) { + abortUnsupportedPlatform(integration); + } + await detectPostHogIntegration(ctx); +} const base = createSkillProgram({ // The menu ids this skill `-`, and context-mill's @@ -159,36 +120,40 @@ const base = createSkillProgram({ * aborting. * * Departures from a plain `createSkillProgram`: - * - `DETECT_STEP` in front, so `session.skillId` carries the framework id the + * - `onReady` detection, so `session.skillId` carries the framework id the * orchestrator's preflight resolves reference + mini-skill variants with. * - `agentFlow` pinned (the id would default to the same value — explicit so * renaming the program can't silently detach the flow). * - `ciPreRun` mirrors the default integration program: scope the install dir * to the right project (monorepos), then detect the framework — the - * headless equivalent of the detect step's onReady hook. + * headless equivalent of `onReady`. */ -export const replayVisionConfig: ProgramConfig = { +export const config: ProgramConfig = { ...base, + binding: { + sequence: Sequence.orchestrator, + harness: Harness.anthropic, + model: DEFAULT_AGENT_MODEL, + }, agentFlow: 'replay-vision', - steps: [DETECT_STEP, ...AGENT_SKILL_STEPS], + onReady: detectReplayVisionProject, + oauthScopeAdditions: REPLAY_VISION_SCOPE_ADDITIONS, ciPreRun: async ( - session: WizardSession, + session: ProgramSession, runner: CiRunnerContext, ): Promise => { await scopeInstallDirToProject(session, runner); const integration = await detectFramework(session.installDir); if (!integration) { - await wizardAbort({ + throw new ProgramAbort({ code: ErrorCodes.DetectNoFramework, message: 'Could not auto-detect your framework for this project.', }); - return; } if (!REPLAY_VISION_SUPPORTED.has(integration)) { - await abortUnsupportedPlatform(integration); - return; + abortUnsupportedPlatform(integration); } session.integration = integration; analytics.setTag('integration', integration); @@ -210,5 +175,6 @@ export const replayVisionConfig: ProgramConfig = { session.frameworkContext[key] = value; } } + noteDetectedFramework(session, frameworkConfig, context, runner.log); }, }; diff --git a/src/programs/revenue-analytics/detect.ts b/src/programs/revenue-analytics/detect.ts index ca868a2de..72fd1ef4f 100644 --- a/src/programs/revenue-analytics/detect.ts +++ b/src/programs/revenue-analytics/detect.ts @@ -6,16 +6,16 @@ */ import { existsSync, statSync } from 'fs'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { ProgramSession } from '../program-session'; import type { AbortCase } from '@agent/types'; -import { findPackageJsons } from '@programs/shared/package-scanning'; +import { findPackageJsons } from '../shared/package-scanning'; export { findPackageJsons, POSTHOG_SDKS, STRIPE_SDKS, type PackageMatch, -} from '@programs/shared/package-scanning'; +} from '../shared/package-scanning'; /** * Structured detection errors. The screen renders each kind into JSX @@ -64,7 +64,7 @@ export const REVENUE_ABORT_CASES: AbortCase[] = [ * The skill install happens later in the bootstrap runner, not here. */ export function detectRevenuePrerequisites( - session: WizardSession, + session: ProgramSession, setFrameworkContext: (key: string, value: unknown) => void, ): void { const fail = (error: RevenueDetectError) => diff --git a/src/programs/revenue-analytics/index.ts b/src/programs/revenue-analytics/index.ts index fb61f831b..0173eb344 100644 --- a/src/programs/revenue-analytics/index.ts +++ b/src/programs/revenue-analytics/index.ts @@ -1,16 +1,14 @@ -import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramConfig } from '../program-step'; import { WIZARD_TOOL_NAMES } from '@agent'; -import { REVENUE_ANALYTICS_PROGRAM } from '../../tui/programs/revenue-analytics/flow.js'; -import { REVENUE_ABORT_CASES } from './detect.js'; -import { getContentBlocks } from '../../tui/programs/revenue-analytics/deck/index.js'; +import { detectRevenuePrerequisites, REVENUE_ABORT_CASES } from './detect.js'; -export const revenueAnalyticsConfig: ProgramConfig = { +export const config: ProgramConfig = { command: 'revenue-analytics', description: 'Set up PostHog for Revenue Analytics', id: 'revenue-analytics-setup', skillId: 'revenue-analytics-setup', - steps: REVENUE_ANALYTICS_PROGRAM, - getContentBlocks, + onReady: (ctx) => + detectRevenuePrerequisites(ctx.session, ctx.setFrameworkContext), allowedTools: ['Agent'], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], run: { @@ -27,7 +25,6 @@ export const revenueAnalyticsConfig: ProgramConfig = { requires: ['posthog-integration'], }; -export { REVENUE_ANALYTICS_PROGRAM } from '../../tui/programs/revenue-analytics/flow.js'; export { detectRevenuePrerequisites, POSTHOG_SDKS, diff --git a/src/programs/self-driving/detect-agentic.ts b/src/programs/self-driving/detect-agentic.ts index 22b3bb47b..700467f4b 100644 --- a/src/programs/self-driving/detect-agentic.ts +++ b/src/programs/self-driving/detect-agentic.ts @@ -14,14 +14,17 @@ import type { AgenticDetectionReport, DetectEvent, -} from '@programs/detection/agentic'; + DetectProgress, +} from '../detection/agentic'; import { detectIntegrationProjects, toIntegrationCandidates, -} from '@programs/detection/project-scope'; -import { gatherFrameworkContext } from '@programs/detection/index'; +} from '../detection/project-scope'; +import { gatherFrameworkContext } from '../detection/context'; +import { noteDetectedFramework } from '../detection/detected-framework'; import type { Integration } from '@shared/constants'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { ProgramSession } from '../program-session'; +import type { RunnerContext } from '../runner-context'; export type { DetectEvent }; @@ -83,12 +86,14 @@ export function toIntegrationReport( /** Run the Haiku detector over the repo and classify projects for integration. */ export async function detectSelfDrivingIntegrationProjects( - session: WizardSession, + session: ProgramSession, onEvent?: DetectEvent, + onProgress?: DetectProgress, ): Promise { const report = await detectIntegrationProjects(session, { programId: 'self-driving', onEvent, + onProgress, }); return toIntegrationReport(report); } @@ -102,7 +107,8 @@ export async function detectSelfDrivingIntegrationProjects( * integrate-run step's `onRunPrep`. */ export async function prepSelfDrivingIntegration( - session: WizardSession, + session: ProgramSession, + log: RunnerContext['log'], ): Promise { // `session` is the phase's derived session — its installDir is already the // picked project (the integrate-run step's `targetDir`), so just gather that @@ -123,4 +129,5 @@ export async function prepSelfDrivingIntegration( session.frameworkContext[key] = value; } } + noteDetectedFramework(session, frameworkConfig, context, log); } diff --git a/src/programs/self-driving/detect.ts b/src/programs/self-driving/detect.ts index 3098a2762..2b527b90e 100644 --- a/src/programs/self-driving/detect.ts +++ b/src/programs/self-driving/detect.ts @@ -28,11 +28,11 @@ import { } from 'fs'; import { join } from 'path'; import { analytics } from '@utils/analytics'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { ProgramSession } from '../program-session'; import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; -import { detectWarehouseSources } from '@programs/warehouse-sources/detect'; -import type { DetectedSource } from '@programs/warehouse-sources/types'; +import { detectWarehouseSources } from '../warehouse-sources/detect'; +import type { DetectedSource } from '../warehouse-sources/types'; /** frameworkContext key holding the deterministic PostHog-presence result. */ export const POSTHOG_PRESENT_KEY = 'postHogPresent'; @@ -50,7 +50,7 @@ export const SELF_DRIVING_DETECTED_TOOLS_KEY = 'selfDrivingDetectedTools'; /** Read the detected tools out of frameworkContext. */ export function getSelfDrivingDetectedTools( - session: WizardSession, + session: ProgramSession, ): DetectedSource[] { return ( (session.frameworkContext[SELF_DRIVING_DETECTED_TOOLS_KEY] as @@ -290,7 +290,7 @@ export const SELF_DRIVING_ABORT_CASES: AbortCase[] = [ * screen renders it and blocks. */ export function detectSelfDrivingPrerequisites( - session: WizardSession, + session: ProgramSession, setFrameworkContext: (key: string, value: unknown) => void, ): void { const fail = (error: SelfDrivingDetectError) => diff --git a/src/programs/self-driving/index.ts b/src/programs/self-driving/index.ts index 94808a7bb..a32f0dabe 100644 --- a/src/programs/self-driving/index.ts +++ b/src/programs/self-driving/index.ts @@ -1,22 +1,29 @@ import { join } from 'path'; import { access, rm } from 'node:fs/promises'; -import type { ProgramConfig } from '@programs/program-step'; -import type { ProgramRun } from '@programs/program-run'; -import { - OutroKind, - type WizardSession, -} from '@programs/session/wizard-session'; +import type { ProgramConfig } from '../program-step'; +import type { ProgramRun } from '../program-run'; import { createSkillProgram } from '../shared/skill-program.js'; -import { SELF_DRIVING_PROGRAM } from '../../tui/programs/self-driving/flow.js'; +import { OutroKind } from '@shared/outro'; +import type { ProgramSession } from '../program-session'; import { - SELF_DRIVING_ABORT_CASES, + detectSelfDrivingPrerequisites, getSelfDrivingDetectedTools, + SELF_DRIVING_ABORT_CASES, + SELF_DRIVING_INTEGRATE_PATH_KEY, } from './detect.js'; import { buildSelfDrivingPrompt } from './prompt.js'; import { resolveSelfDrivingStepKey } from './step-keys.js'; +import { resolveProjectDir } from '../detection/agentic'; +import { prepSelfDrivingIntegration } from './detect-agentic.js'; import { NO_DEFAULT_LIMIT, PRICE_PER_PR_USD, PRICING_LONG } from './pricing.js'; -import { getTips } from '../../tui/programs/self-driving/deck/tips.js'; -import { getContentBlocks } from '../../tui/programs/self-driving/deck/index.js'; +import { SELF_DRIVING_SCOPE_ADDITIONS } from './scopes.js'; + +/** Absolute dir to integrate into: the picked sub-app (LLM output — the shared resolver clamps escapes), else the repo root. */ +const integrationDir = (session: ProgramSession): string => + resolveProjectDir( + session.installDir, + session.frameworkContext[SELF_DRIVING_INTEGRATE_PATH_KEY], + ); export const SELF_DRIVING_SKILL_ID = 'self-driving-setup'; const REPORT_FILE = 'posthog-self-driving-report.md'; @@ -46,7 +53,7 @@ async function removeInstalledSkill(installDir: string): Promise { // A session closure (not a static object) so `customPrompt` can read the // tools detected in the codebase — written to frameworkContext by the detect // step — and hand them to the prompt for STEP 4/STEP 5 prioritisation. -const buildRun = (session: WizardSession): Promise => +const buildRun = (session: ProgramSession): Promise => Promise.resolve({ skillId: SELF_DRIVING_SKILL_ID, integrationLabel: SELF_DRIVING_SKILL_ID, @@ -110,7 +117,7 @@ const buildRun = (session: WizardSession): Promise => }, }); -export const selfDrivingConfig: ProgramConfig = { +export const config: ProgramConfig = { ...createSkillProgram({ skillId: SELF_DRIVING_SKILL_ID, command: 'self-driving', @@ -125,15 +132,34 @@ export const selfDrivingConfig: ProgramConfig = { requires: ['posthog-integration'], abortCases: SELF_DRIVING_ABORT_CASES, }), - steps: SELF_DRIVING_PROGRAM, + onReady: (ctx) => + detectSelfDrivingPrerequisites(ctx.session, ctx.setFrameworkContext), + oauthScopeAdditions: SELF_DRIVING_SCOPE_ADDITIONS, + // Integrate path: posthog-integration's agent runs composed, in the picked + // project's dir, with that project's framework context gathered first. + runSteps: { + 'integrate-run': { + runProgramId: 'posthog-integration', + onRunPrep: prepSelfDrivingIntegration, + targetDir: integrationDir, + }, + }, run: buildRun, - getTips, - getContentBlocks, }; -export { SELF_DRIVING_PROGRAM } from '../../tui/programs/self-driving/flow.js'; export { detectSelfDrivingPrerequisites, + GITHUB_REQUIRED_BODY, + GITHUB_REQUIRED_MESSAGE, + POSTHOG_PRESENT_KEY, SELF_DRIVING_ABORT_CASES, + SELF_DRIVING_INTEGRATE_PATH_KEY, type SelfDrivingDetectError, } from './detect.js'; + +export { NO_DEFAULT_LIMIT, PRICING_LONG, PRICING_SHORT } from './pricing.js'; +export { + detectSelfDrivingIntegrationProjects, + type IntegrationDetectionReport, + type IntegrationProject, +} from './detect-agentic.js'; diff --git a/src/programs/self-driving/prompt.ts b/src/programs/self-driving/prompt.ts index 393586dc4..0672c1315 100644 --- a/src/programs/self-driving/prompt.ts +++ b/src/programs/self-driving/prompt.ts @@ -1,6 +1,6 @@ import { AgentSignals } from '@agent'; import type { PromptContext } from '@agent/types'; -import type { DetectedSource } from '@programs/warehouse-sources/types'; +import type { DetectedSource } from '../warehouse-sources/types'; /** * Render the deterministic codebase-tool scan for the prompt. STEP 4 and diff --git a/src/programs/warehouse-source/detect.ts b/src/programs/warehouse-source/detect.ts index ac847f9f6..7266c0768 100644 --- a/src/programs/warehouse-source/detect.ts +++ b/src/programs/warehouse-source/detect.ts @@ -8,10 +8,12 @@ import { existsSync, statSync } from 'fs'; import { analytics } from '@utils/analytics'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { ProgramSession } from '../program-session'; import type { AbortCase } from '@agent/types'; -import { detectWarehouseSources } from '@programs/warehouse-sources/detect'; -import type { DetectedSource } from '@programs/warehouse-sources/types'; +import { + detectWarehouseSources, + DETECTED_WAREHOUSE_SOURCES_KEY, +} from '../warehouse-sources/detect'; /** Structured detection errors rendered by the intro screen. */ export type WarehouseDetectError = @@ -22,23 +24,6 @@ export type WarehouseDetectError = } | { kind: 'no-sources' }; -/** frameworkContext key holding the detected sources (set on success). */ -export const DETECTED_WAREHOUSE_SOURCES_KEY = 'detectedWarehouseSources'; - -/** - * Read the detected sources out of frameworkContext. Single accessor shared by - * the intro screen and the prompt builder so the key + cast live in one place. - */ -export function getDetectedWarehouseSources( - session: WizardSession, -): DetectedSource[] { - return ( - (session.frameworkContext[DETECTED_WAREHOUSE_SOURCES_KEY] as - | DetectedSource[] - | undefined) ?? [] - ); -} - /** `[ABORT] ` cases the skill can emit. */ export const WAREHOUSE_ABORT_CASES: AbortCase[] = [ { @@ -69,7 +54,7 @@ export const WAREHOUSE_ABORT_CASES: AbortCase[] = [ * sources (or a `detectError`) into frameworkContext for the intro screen. */ export function detectWarehousePrerequisites( - session: WizardSession, + session: ProgramSession, setFrameworkContext: (key: string, value: unknown) => void, ): void { const fail = (error: WarehouseDetectError) => diff --git a/src/programs/warehouse-source/index.ts b/src/programs/warehouse-source/index.ts index 714ead249..a0572156c 100644 --- a/src/programs/warehouse-source/index.ts +++ b/src/programs/warehouse-source/index.ts @@ -1,20 +1,20 @@ -import type { ProgramConfig } from '@programs/program-step'; -import type { ProgramRun } from '@programs/program-run'; -import type { WizardSession } from '@programs/session/wizard-session'; -import { LONGER_ASK_TIMEOUT_MS } from '@agent'; -import { WAREHOUSE_SOURCE_PROGRAM } from '../../tui/programs/warehouse-source/flow.js'; +import type { ProgramConfig } from '../program-step'; +import type { ProgramRun } from '../program-run'; +import type { ProgramSession } from '../program-session'; +import { LONGER_ASK_TIMEOUT_MS } from '@shared/ask-policy'; +import { WAREHOUSE_SOURCE_SCOPE_ADDITIONS } from '../oauth/program-scopes'; import { + detectWarehousePrerequisites, WAREHOUSE_ABORT_CASES, - getDetectedWarehouseSources, } from './detect.js'; -import { getContentBlocks } from '../../tui/programs/warehouse-source/deck/index.js'; +import { getDetectedWarehouseSources } from '../warehouse-sources/detect'; /** * Inject the detected sources (and their creation mode) into the prompt so the * skill knows what to set up. The *how* — in-CLI creation vs deep-link, field * collection, validation — lives in the skill, not here. */ -function buildPrompt(session: WizardSession): string { +function buildPrompt(session: ProgramSession): string { const sources = getDetectedWarehouseSources(session); if (sources.length === 0) { return 'Set up a data warehouse source for this project.'; @@ -41,16 +41,19 @@ function buildPrompt(session: WizardSession): string { ].join('\n'); } -export const warehouseSourceConfig: ProgramConfig = { +export const config: ProgramConfig = { command: 'warehouse', description: 'Detect and connect Data Warehouse sources', id: 'warehouse-source', skillId: 'data-warehouse-source-setup', - steps: WAREHOUSE_SOURCE_PROGRAM, - getContentBlocks, + onReady: (ctx) => + detectWarehousePrerequisites(ctx.session, ctx.setFrameworkContext), + oauthScopeAdditions: WAREHOUSE_SOURCE_SCOPE_ADDITIONS, + // No health-check screen in the TUI flow; the run skips the readiness check. + healthCheck: false, reportFile: 'posthog-warehouse-report.md', allowedTools: ['Agent'], - run: (session: WizardSession): Promise => + run: (session: ProgramSession): Promise => Promise.resolve({ skillId: 'data-warehouse-source-setup', integrationLabel: 'data-warehouse-source-setup', @@ -70,11 +73,12 @@ export const warehouseSourceConfig: ProgramConfig = { requires: ['posthog-integration'], }; -export { WAREHOUSE_SOURCE_PROGRAM } from '../../tui/programs/warehouse-source/flow.js'; export { detectWarehousePrerequisites, - getDetectedWarehouseSources, - DETECTED_WAREHOUSE_SOURCES_KEY, WAREHOUSE_ABORT_CASES, type WarehouseDetectError, } from './detect.js'; +export { + getDetectedWarehouseSources, + DETECTED_WAREHOUSE_SOURCES_KEY, +} from '../warehouse-sources/detect'; diff --git a/src/programs/web-analytics-doctor/detect.ts b/src/programs/web-analytics-doctor/detect.ts index d96321b89..af3f0a7b0 100644 --- a/src/programs/web-analytics-doctor/detect.ts +++ b/src/programs/web-analytics-doctor/detect.ts @@ -1,8 +1,8 @@ import { existsSync, statSync } from 'fs'; -import type { WizardSession } from '@programs/session/wizard-session'; +import type { ProgramSession } from '../program-session'; import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; -import { findPackageJsons } from '@programs/shared/package-scanning'; +import { findPackageJsons } from '../shared/package-scanning'; export type WebAnalyticsDetectError = | { @@ -46,7 +46,7 @@ export const WEB_ANALYTICS_ABORT_CASES: AbortCase[] = [ ]; export function detectWebAnalyticsPrerequisites( - session: WizardSession, + session: ProgramSession, setFrameworkContext: (key: string, value: unknown) => void, ): void { const fail = (error: WebAnalyticsDetectError) => diff --git a/src/programs/web-analytics-doctor/index.ts b/src/programs/web-analytics-doctor/index.ts index c35aa3bc7..c7e72f9e2 100644 --- a/src/programs/web-analytics-doctor/index.ts +++ b/src/programs/web-analytics-doctor/index.ts @@ -1,12 +1,14 @@ -import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramConfig } from '../program-step'; import { createSkillProgram } from '../shared/skill-program.js'; -import { WEB_ANALYTICS_DOCTOR_PROGRAM } from './steps.js'; -import { WEB_ANALYTICS_ABORT_CASES } from './detect.js'; +import { + detectWebAnalyticsPrerequisites, + WEB_ANALYTICS_ABORT_CASES, +} from './detect.js'; const REPORT_FILE = 'posthog-web-analytics-report.md'; const DOCS_URL = 'https://posthog.com/docs/web-analytics'; -export const webAnalyticsDoctorConfig: ProgramConfig = { +export const config: ProgramConfig = { ...createSkillProgram({ skillId: 'web-analytics-doctor', command: 'web-analytics', @@ -28,11 +30,11 @@ export const webAnalyticsDoctorConfig: ProgramConfig = { requires: ['posthog-integration'], abortCases: WEB_ANALYTICS_ABORT_CASES, }), - steps: WEB_ANALYTICS_DOCTOR_PROGRAM, + onReady: (ctx) => + detectWebAnalyticsPrerequisites(ctx.session, ctx.setFrameworkContext), parentCommand: 'audit', }; -export { WEB_ANALYTICS_DOCTOR_PROGRAM } from './steps.js'; export { detectWebAnalyticsPrerequisites, WEB_ANALYTICS_ABORT_CASES,