diff --git a/bin.ts b/bin.ts index 2a3d25974..17e41e8e4 100644 --- a/bin.ts +++ b/bin.ts @@ -83,7 +83,7 @@ import { uploadSourcemapsCommand } from './src/commands/upload-sourcemaps'; import { errorTrackingCommand } from './src/commands/error-tracking'; import { skillCommand } from './src/commands/skill'; import { cliCommand } from './src/commands/cli'; -import { recoverOrphanedSettingsBackups } from './src/lib/agent/claude-settings'; +import { recoverOrphanedSettingsBackups } from './src/lib/claude-settings'; // Heal any .claude/settings backup a previous interrupted run left orphaned, // before anything else reads Claude settings — conflict detection, OAuth, and diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index ded2d55a2..90c744e09 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -28,6 +28,7 @@ import { buildSession } from '@lib/wizard-session'; import { initLocalDev } from '@lib/local-dev'; import { configureGatewayFromCIEnvironment } from '@lib/gateway-session'; import { runAgent } from '@lib/agent/agent-runner'; +import { runConfigFor } from '@lib/programs/run-config'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; import { getAuditChecks } from '@lib/programs/audit/types'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; @@ -331,16 +332,23 @@ async function main() { if (step.screenId === 'auth') { await authenticate(store.session, programConfig.id); } else if (step.run) { - await step.run(await runSessionFor(step)); + await runAgent( + runConfigFor(getProgramConfig(step.run.programId)), + await runSessionFor(step), + { composed: true }, + ); store.completeRunStep(step.id); } else if (step.screenId === 'run') { - await runAgent(programConfig, await runSessionFor(step)); + await runAgent( + runConfigFor(programConfig), + await runSessionFor(step), + ); } else if (step.isComplete) { await store.waitUntil(step.isComplete); } } } else { - await runAgent(programConfig, store.session); + await runAgent(runConfigFor(programConfig), store.session); } }; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index d523b2a73..017c5c5b9 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -5,52 +5,27 @@ "src/lib/agent/mcp-prompt-streaming.ts -> src/ui/tui/services/mcp-suggested-prompts-services.ts", "src/lib/detection/agentic.ts -> src/lib/agent/agent-interface.ts", "src/lib/detection/project-scope.ts -> src/lib/agent/runner/shared/authenticate.ts", - "src/lib/errors/agent-map.ts -> src/lib/agent/signals.ts", - "src/lib/programs/agent-skill/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/agent-skill/index.ts -> src/lib/programs/agent-skill/content/index.tsx", "src/lib/programs/ai-observability/index.ts -> src/lib/programs/agent-skill/content/index.tsx", - "src/lib/programs/audit/detect.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/audit/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/dispatch-family.ts -> src/commands/command.ts", "src/lib/programs/dispatch-family.ts -> src/commands/factories/shared.ts", - "src/lib/programs/error-tracking-upload-source-maps/detect.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/error-tracking-upload-source-maps/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/error-tracking-upload-source-maps/index.ts -> src/lib/programs/error-tracking-upload-source-maps/content/index.tsx", - "src/lib/programs/error-tracking-upload-source-maps/prompt.ts -> src/lib/agent/agent-interface.ts", - "src/lib/programs/error-tracking/index.ts -> src/lib/agent/runner/shared/types.ts", "src/lib/programs/error-tracking/index.ts -> src/lib/programs/error-tracking/content/index.tsx", "src/lib/programs/error-tracking/index.ts -> src/lib/programs/error-tracking/content/tips.ts", - "src/lib/programs/events-audit/index.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/mcp-analytics/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/metrics/index.ts -> src/lib/programs/agent-skill/content/index.tsx", - "src/lib/programs/migration/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/migration/index.ts -> src/lib/programs/migration/content/index.tsx", - "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/agent-interface.ts", - "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/runner/shared/bootstrap.ts", "src/lib/programs/posthog-integration/index.ts -> src/lib/programs/posthog-integration/content/index.tsx", "src/lib/programs/program-registry.ts -> src/lib/programs/agent-skill/content/index.tsx", - "src/lib/programs/program-step.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/program-step.ts -> src/ui/tui/components/TipsCard.tsx", "src/lib/programs/program-step.ts -> src/ui/tui/primitives/index.ts", "src/lib/programs/program-step.ts -> src/ui/tui/store.ts", - "src/lib/programs/replay-vision/index.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/revenue-analytics/detect.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/revenue-analytics/index.ts -> src/lib/programs/revenue-analytics/content/index.tsx", - "src/lib/programs/self-driving/detect.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/self-driving/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/self-driving/index.ts -> src/lib/programs/self-driving/content/index.tsx", "src/lib/programs/self-driving/index.ts -> src/lib/programs/self-driving/content/pricing.ts", "src/lib/programs/self-driving/index.ts -> src/lib/programs/self-driving/content/tips.ts", - "src/lib/programs/self-driving/prompt.ts -> src/lib/agent/agent-interface.ts", - "src/lib/programs/self-driving/prompt.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/warehouse-source/detect.ts -> src/lib/agent/agent-runner.ts", - "src/lib/programs/warehouse-source/index.ts -> src/lib/agent/agent-runner.ts", "src/lib/programs/warehouse-source/index.ts -> src/lib/programs/warehouse-source/content/index.tsx", - "src/lib/programs/web-analytics-doctor/detect.ts -> src/lib/agent/agent-runner.ts", "src/lib/task-stream/event-plan-watcher.ts -> src/ui/tui/store.ts", "src/lib/task-stream/task-stream-push.ts -> src/ui/tui/store.ts", - "src/lib/wizard-session.ts -> src/lib/agent/claude-settings.ts", "src/lib/wizard-tools/index.ts -> src/lib/wizard-tools/mcp.ts", "src/lib/wizard-tools/tools.ts -> src/lib/yara-hooks.ts", "src/steps/add-mcp-server-to-clients/index.ts -> src/telemetry.ts", @@ -58,19 +33,10 @@ "src/steps/run-prettier.ts -> src/telemetry.ts", "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", "src/ui/index.ts -> src/ui/logging-ui.ts", - "src/ui/logging-ui.ts -> src/lib/agent/claude-settings.ts", "src/ui/tui/components/PhaseVisuals.tsx -> src/lib/agent/agent-phase.ts", - "src/ui/tui/components/TokenCostHud.tsx -> src/lib/agent/token-pricing.ts", - "src/ui/tui/exit-line.ts -> src/lib/agent/token-pricing.ts", - "src/ui/tui/ink-ui.ts -> src/lib/agent/claude-settings.ts", "src/ui/tui/playground/demos/RunScreenDemo.tsx -> src/lib/agent/agent-phase.ts", - "src/ui/tui/screens/ManagedSettingsScreen.tsx -> src/lib/agent/claude-settings.ts", "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/lib/agent/mcp-prompt-streaming.ts", - "src/ui/tui/store.ts -> src/lib/agent/claude-settings.ts", - "src/ui/tui/store.ts -> src/lib/agent/token-pricing.ts", - "src/ui/wizard-ui.ts -> src/lib/agent/claude-settings.ts", "src/utils/package-manager.ts -> src/telemetry.ts", - "src/utils/setup-utils.ts -> src/telemetry.ts", - "src/utils/wizard-abort.ts -> src/ui/logging-ui.ts" + "src/utils/setup-utils.ts -> src/telemetry.ts" ] } diff --git a/src/lib/agent/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap b/src/lib/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap similarity index 100% rename from src/lib/agent/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap rename to src/lib/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap diff --git a/src/lib/agent/__tests__/agent-env-isolation.test.ts b/src/lib/__tests__/agent-env-isolation.test.ts similarity index 99% rename from src/lib/agent/__tests__/agent-env-isolation.test.ts rename to src/lib/__tests__/agent-env-isolation.test.ts index 29a1feaf3..d68f01c09 100644 --- a/src/lib/agent/__tests__/agent-env-isolation.test.ts +++ b/src/lib/__tests__/agent-env-isolation.test.ts @@ -2,7 +2,7 @@ import { sanitizeAgentSubprocessEnv, isBlockedAgentEnvKey, BLOCKED_AGENT_ENV_KEYS, -} from '@lib/agent/agent-env-isolation'; +} from '@lib/agent-env-isolation'; describe('isBlockedAgentEnvKey', () => { it('blocks the direct API key that outranks the gateway auth token', () => { diff --git a/src/lib/__tests__/agent-interface.test.ts b/src/lib/__tests__/agent-interface.test.ts index 3c48975be..181382b56 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/lib/__tests__/agent-interface.test.ts @@ -10,7 +10,7 @@ import { reportMcpSetup, } from '@lib/agent/agent-interface'; import { AgentOutputSignals } from '@lib/agent/output-signals'; -import { RESUME_INSTRUCTION } from '@lib/agent/signals'; +import { RESUME_INSTRUCTION } from '@lib/agent-signals'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; import { Sequence } from '@lib/constants'; diff --git a/src/lib/agent/__tests__/claude-settings-backup.test.ts b/src/lib/__tests__/claude-settings-backup.test.ts similarity index 99% rename from src/lib/agent/__tests__/claude-settings-backup.test.ts rename to src/lib/__tests__/claude-settings-backup.test.ts index 225c9696e..8dd9815e0 100644 --- a/src/lib/agent/__tests__/claude-settings-backup.test.ts +++ b/src/lib/__tests__/claude-settings-backup.test.ts @@ -18,7 +18,7 @@ import { backupAndFixClaudeSettings, restoreClaudeSettings, recoverOrphanedSettingsBackups, -} from '@lib/agent/claude-settings'; +} from '@lib/claude-settings'; const SETTINGS = 'settings.json'; const BACKUP = 'settings.json.wizard-backup'; diff --git a/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts b/src/lib/__tests__/credential-isolation-snapshot.test.ts similarity index 96% rename from src/lib/agent/__tests__/credential-isolation-snapshot.test.ts rename to src/lib/__tests__/credential-isolation-snapshot.test.ts index a17d5ad04..e0ee0d4e7 100644 --- a/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts +++ b/src/lib/__tests__/credential-isolation-snapshot.test.ts @@ -13,9 +13,9 @@ * isolation surface — a diff here means a credential path changed disposition. */ -import { sanitizeAgentSubprocessEnv } from '@lib/agent/agent-env-isolation'; -import { classifySettingsConflicts } from '@lib/agent/claude-settings'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import { sanitizeAgentSubprocessEnv } from '@lib/agent-env-isolation'; +import { classifySettingsConflicts } from '@lib/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; // Every env-based avenue (one key each), plus the gateway routing and benign // env that must survive. Grouped by avenue for readability; the snapshot sorts. diff --git a/src/lib/agent/__tests__/settings-conflicts.test.ts b/src/lib/__tests__/settings-conflicts.test.ts similarity index 98% rename from src/lib/agent/__tests__/settings-conflicts.test.ts rename to src/lib/__tests__/settings-conflicts.test.ts index 31f78f3e6..2c71f14c2 100644 --- a/src/lib/agent/__tests__/settings-conflicts.test.ts +++ b/src/lib/__tests__/settings-conflicts.test.ts @@ -10,8 +10,8 @@ import { checkAllSettingsConflicts, managedSettingsPath, classifySettingsConflicts, -} from '@lib/agent/claude-settings'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +} from '@lib/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; import { buildAuthErrorContext } from '@lib/agent/agent-interface'; const OVERRIDE = JSON.stringify({ apiKeyHelper: 'echo sk-x' }); diff --git a/src/lib/agent/__tests__/token-pricing.test.ts b/src/lib/__tests__/token-pricing.test.ts similarity index 99% rename from src/lib/agent/__tests__/token-pricing.test.ts rename to src/lib/__tests__/token-pricing.test.ts index 69bed5e38..4a713cb19 100644 --- a/src/lib/agent/__tests__/token-pricing.test.ts +++ b/src/lib/__tests__/token-pricing.test.ts @@ -3,7 +3,7 @@ import { pricePerMtokForModel, formatTokenCount, formatCostUsd, -} from '@lib/agent/token-pricing'; +} from '@lib/token-pricing'; describe('pricePerMtokForModel', () => { it('defaults to Sonnet pricing (DEFAULT_AGENT_MODEL) when no model is given', () => { diff --git a/src/lib/agent/agent-env-isolation.ts b/src/lib/agent-env-isolation.ts similarity index 100% rename from src/lib/agent/agent-env-isolation.ts rename to src/lib/agent-env-isolation.ts diff --git a/src/lib/agent/signals.ts b/src/lib/agent-signals.ts similarity index 100% rename from src/lib/agent/signals.ts rename to src/lib/agent-signals.ts diff --git a/src/lib/agent/__tests__/output-signals.test.ts b/src/lib/agent/__tests__/output-signals.test.ts index 76eeaef6c..cb6870e66 100644 --- a/src/lib/agent/__tests__/output-signals.test.ts +++ b/src/lib/agent/__tests__/output-signals.test.ts @@ -1,5 +1,5 @@ import { AgentOutputSignals } from '@lib/agent/output-signals'; -import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent/signals'; +import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent-signals'; describe('REMARK_INSTRUCTION', () => { it('carries the marker but no literal placeholder a model could echo', () => { diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 815498a25..cd41e82b4 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -53,28 +53,28 @@ import { AgentErrorType, REMARK_INSTRUCTION, RESUME_INSTRUCTION, -} from './signals'; +} from '@lib/agent-signals'; import { classifyAuthFailure } from '@lib/errors'; import { isGrantRevoked } from '@lib/auth-session-state'; import { AgentOutputSignals } from './output-signals'; // Signal vocabulary and the output parser live in dedicated modules; re-export // so existing importers of these from agent-interface keep working. -export { AgentSignals, AgentErrorType } from './signals'; -export type { AgentSignal } from './signals'; +export { AgentSignals, AgentErrorType } from '@lib/agent-signals'; +export type { AgentSignal } from '@lib/agent-signals'; export { AgentOutputSignals } from './output-signals'; import { checkAllSettingsConflicts, type SettingsConflict, type SettingsConflictSource, -} from './claude-settings'; +} from '@lib/claude-settings'; import { detectStoredClaudeLogin, hasStoredClaudeLogin, claudeConfigDir, createIsolatedAgentConfigDir, } from './stored-login'; -import { sanitizeAgentSubprocessEnv } from './agent-env-isolation'; +import { sanitizeAgentSubprocessEnv } from '@lib/agent-env-isolation'; // Dynamic import cache for ESM module let _sdkModule: any = null; diff --git a/src/lib/agent/agent-prompt.ts b/src/lib/agent/agent-prompt.ts index 0427fc4f5..8e684322b 100644 --- a/src/lib/agent/agent-prompt.ts +++ b/src/lib/agent/agent-prompt.ts @@ -7,37 +7,9 @@ * 3. Skill prompt — "follow SKILL.md" instructions (if a skill was installed) */ -import type { ProgramRun } from './agent-runner.js'; -import type { HostResolution } from '@lib/host-resolution'; +import type { ProgramRun, PromptContext } from '@lib/program-run'; -/** - * Values available to prompt builders after OAuth completes. - */ -export interface PromptContext { - projectId: number; - projectApiKey: string; - host: HostResolution; - /** Set when skillId was provided and the skill was installed successfully. */ - skillPath?: string; - /** - * Org-level AI consent (`is_ai_data_processing_approved`) read from the - * `/api/users/@me/` payload at auth time. `null` = unknown (older orgs, - * or the user fetch failed). Lets prompts pre-resolve consent state so - * agents only ask the user when it is actually off or unknown. - */ - orgAiDataProcessingApproved?: boolean | null; - /** - * Team product opt-ins from the `/api/projects/:id/` payload at auth - * time. Project-level truth for "is this product enabled" — products - * can be instrumented from other repos or the snippet, so repo-local - * evidence must never rule them out. `null` field = unknown. - */ - teamProductOptIns?: { - sessionReplay?: boolean | null; - exceptionAutocapture?: boolean | null; - surveys?: boolean | null; - } | null; -} +export type { PromptContext }; function defaultProjectPrompt(ctx: PromptContext): string { return `You have access to the PostHog MCP server. diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts index 8b92c4ebe..f8fd5a00b 100644 --- a/src/lib/agent/agent-runner.ts +++ b/src/lib/agent/agent-runner.ts @@ -8,6 +8,7 @@ export { runProgram, shouldDisableAsk, type ProgramRun, + type ProgramRunConfig, type BootstrapResult, type AbortCase, type PromptContext, diff --git a/src/lib/agent/mcp-prompt-streaming.ts b/src/lib/agent/mcp-prompt-streaming.ts index 1b9e55182..f0f6e87a9 100644 --- a/src/lib/agent/mcp-prompt-streaming.ts +++ b/src/lib/agent/mcp-prompt-streaming.ts @@ -18,7 +18,7 @@ import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@lib/constants'; import { logToFile } from '@utils/debug'; import { gatewayAuth } from '@lib/gateway-session'; import { buildAgentEnv, buildRunTags } from '@lib/agent/agent-interface'; -import { sanitizeAgentSubprocessEnv } from '@lib/agent/agent-env-isolation'; +import { sanitizeAgentSubprocessEnv } from '@lib/agent-env-isolation'; import { createIsolatedAgentConfigDir } from '@lib/agent/stored-login'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/agent/output-signals.ts b/src/lib/agent/output-signals.ts index f5e543c75..c8df7aed6 100644 --- a/src/lib/agent/output-signals.ts +++ b/src/lib/agent/output-signals.ts @@ -12,7 +12,7 @@ * hooks' onTerminate callback (`yaraViolationReason` in runAgent) instead. */ -import { AgentSignals, REMARK_INSTRUCTION } from './signals'; +import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent-signals'; /** * Single source of truth for the substrings runAgent scans agent output for. diff --git a/src/lib/agent/runner/harness/pi/__tests__/completion.test.ts b/src/lib/agent/runner/harness/pi/__tests__/completion.test.ts index a378c2d6d..43eae2772 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/completion.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/completion.test.ts @@ -1,5 +1,5 @@ import { completionFailure, runErrorType } from '../completion'; -import { AgentErrorType } from '@lib/agent/signals'; +import { AgentErrorType } from '@lib/agent-signals'; describe('completionFailure', () => { it('fails a no-op run (zero tool calls) as NO_PROGRESS', () => { diff --git a/src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts b/src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts index b2920ab18..12309fff1 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect } from 'vitest'; -import { AgentSignals } from '@lib/agent/signals'; +import { AgentSignals } from '@lib/agent-signals'; import { lastStatusLine } from '..'; const S = AgentSignals.STATUS; // '[STATUS]' diff --git a/src/lib/agent/runner/harness/pi/completion.ts b/src/lib/agent/runner/harness/pi/completion.ts index 3d4c9d606..a4fed8aa4 100644 --- a/src/lib/agent/runner/harness/pi/completion.ts +++ b/src/lib/agent/runner/harness/pi/completion.ts @@ -1,4 +1,4 @@ -import { AgentErrorType } from '@lib/agent/signals'; +import { AgentErrorType } from '@lib/agent-signals'; /** Which completion guard should fail a pi run, or undefined for a clean finish. */ export function completionFailure(args: { diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index 092e59718..8c6badb5b 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -24,7 +24,7 @@ import { } from '@lib/constants'; import { analytics } from '@utils/analytics'; import { AgentErrorType } from '@lib/agent/agent-interface'; -import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent/signals'; +import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent-signals'; import { AgentOutputSignals } from '@lib/agent/output-signals'; import { assembleCommandments } from '../../switchboard/commandments'; import { gatewayAuth, type GatewayAuth } from '@lib/gateway-session'; diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 37deec62d..7c1acf79f 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -31,7 +31,7 @@ import { renderToolInventory, } from '@lib/agent/agent-prompt-loader'; import { AgentErrorType } from '@lib/agent/agent-interface'; -import { REMARK_INSTRUCTION } from '@lib/agent/signals'; +import { REMARK_INSTRUCTION } from '@lib/agent-signals'; import { AgentOutputSignals } from '@lib/agent/output-signals'; import { TaskStatus } from '../../sequence/orchestrator/queue'; import type { OrchestratorToolsContext } from '../../sequence/orchestrator/queue-tools'; diff --git a/src/lib/agent/runner/harness/types.ts b/src/lib/agent/runner/harness/types.ts index 67c59e5c9..f5fa53099 100644 --- a/src/lib/agent/runner/harness/types.ts +++ b/src/lib/agent/runner/harness/types.ts @@ -18,7 +18,7 @@ import type { WizardSession } from '@lib/wizard-session'; import type { AdditionalFeature } from '@lib/wizard-session'; import type { Harness } from '@lib/constants'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramRunConfig } from '@lib/program-run'; import type { SpinnerHandle } from '@ui'; import type { WizardAskBridge } from '@lib/wizard-ask-bridge'; import type { AgentErrorType } from '@lib/agent/agent-interface'; @@ -46,7 +46,7 @@ export interface RunMiddleware { export interface BackendRunInputs { session: WizardSession; config: ProgramRun; - programConfig: ProgramConfig; + programConfig: ProgramRunConfig; boot: BootstrapResult; /** The fully assembled prompt. */ prompt: string; @@ -76,7 +76,7 @@ export type AgentResult = { error?: AgentErrorType; message?: string }; */ export interface TaskRunInputs { session: WizardSession; - programConfig: ProgramConfig; + programConfig: ProgramRunConfig; boot: BootstrapResult; /** The fully assembled per-task or seed prompt. */ prompt: string; diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index f11bf73c8..f9ee5f3f8 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -2,7 +2,7 @@ * Unified program runner — dispatcher. * * Single configurable pipeline for all programs. Each program - * provides a ProgramRun (via the `run` field on ProgramConfig) + * provides a ProgramRun (via the `run` field on ProgramRunConfig) * that controls: * - Whether a skill is pre-installed or discovered at runtime * - How the agent prompt is built @@ -25,7 +25,7 @@ import { } from '@lib/constants'; import { logToFile } from '@utils/debug'; import { getUI } from '../../../ui'; -import type { ProgramConfig } from '../../programs/program-step'; +import type { ProgramRunConfig } from '@lib/program-run'; import type { ProgramRun, BootstrapResult } from './shared/types'; import { bootstrapProgram } from './shared/bootstrap'; import { @@ -40,6 +40,7 @@ import { registerCleanup } from '../../../utils/wizard-abort'; export type { ProgramRun, + ProgramRunConfig, BootstrapResult, AbortCase, PromptContext, @@ -48,11 +49,11 @@ export type { export { shouldDisableAsk } from './shared/bootstrap'; /** - * Resolve a ProgramConfig's agent run definition and execute the pipeline. + * Resolve a ProgramRunConfig's agent run definition and execute the pipeline. * Entry point for bin.ts — handles buildRunConfig, bootstrap, and (future) run field. */ export async function runAgent( - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, session: WizardSession, options: { composed?: boolean } = {}, ): Promise { @@ -89,7 +90,7 @@ export async function runAgent( export async function runProgram( session: WizardSession, config: ProgramRun, - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, options: { composed?: boolean } = {}, ): Promise { const boot = await bootstrapProgram(session, config, programConfig); @@ -136,7 +137,7 @@ export async function runProgram( */ function resolveProgramRunner( session: WizardSession, - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, boot: BootstrapResult, composed: boolean, ): ProgramBinding { diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index 94ab86841..e7654ac62 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -9,7 +9,7 @@ import type { WizardSession } from '../../../wizard-session'; import { OutroKind } from '../../../wizard-session'; import { getUI } from '../../../../ui'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; -import { restoreClaudeSettings } from '../../claude-settings'; +import { restoreClaudeSettings } from '@lib/claude-settings'; import { logToFile } from '../../../../utils/debug'; import { createBenchmarkPipeline } from '../../../middleware/benchmark'; import { @@ -26,7 +26,7 @@ import { } from '../../../yara-hooks'; import { installSkillById } from '../../../wizard-tools'; import { createWizardAskBridge } from '../../../wizard-ask-bridge'; -import type { ProgramConfig } from '../../../programs/program-step'; +import type { ProgramRunConfig } from '@lib/program-run'; import { assemblePrompt } from '../../agent-prompt'; import type { ProgramRun, BootstrapResult } from '../shared/types'; import { abortOnInstallFailure } from '../shared/errors'; @@ -36,7 +36,7 @@ import { resolveHarness, getHarness } from '../switchboard'; export async function runLinearProgram( session: WizardSession, config: ProgramRun, - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, boot: BootstrapResult, composed = false, ): Promise { diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 8778431ad..0e981e2cd 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -43,7 +43,7 @@ import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; import { wizardAbort, WizardError } from '@utils/wizard-abort'; import { ErrorCodes } from '@lib/errors'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramRunConfig } from '@lib/program-run'; import type { BootstrapResult, ProgramRun } from '../../shared/types'; import { areSeededTasksEnabled, @@ -430,7 +430,7 @@ export function displayOrder( export async function runOrchestrator( session: WizardSession, config: ProgramRun, - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, boot: BootstrapResult, ): Promise { const runId = randomUUID(); diff --git a/src/lib/agent/runner/shared/bootstrap.ts b/src/lib/agent/runner/shared/bootstrap.ts index 6c2bd794e..4924a61a1 100644 --- a/src/lib/agent/runner/shared/bootstrap.ts +++ b/src/lib/agent/runner/shared/bootstrap.ts @@ -20,7 +20,7 @@ import { checkAllSettingsConflicts, backupAndFixClaudeSettings, classifySettingsConflicts, -} from '@lib/agent/claude-settings'; +} from '@lib/claude-settings'; import { evaluateWizardReadiness, WizardReadiness, @@ -36,33 +36,14 @@ import { CallType, getSkillsBaseUrl, IS_DEV } from '@lib/constants'; import { VERSION } from '@lib/version'; import { mcpUrlFor } from '@lib/host-resolution'; import type { WizardRunOptions } from '@utils/types'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramRunConfig } from '@lib/program-run'; +import { shouldDisableAsk } from '@lib/ask-policy'; + +export { shouldDisableAsk }; import type { ProgramRun, BootstrapResult } from './types'; // ── Helpers ────────────────────────────────────────────────────────── -/** - * Decide whether the `wizard_ask` overlay should be wired for this run. - * Disabled in non-interactive modes (CI, signup) — there's no human to - * answer. Per-program disabling is done by adding WIZARD_ASK_TOOL_NAME to - * the program's `disallowedTools` so the SDK rejects calls outright. - * Extracted so the policy can be unit-tested directly. - * - * `session.e2eAsk` is the one escape hatch. The e2e harness runs a `ci` - * session, but it does have an answerer — the driver loop answers each - * `wizard_ask` batch from the program's e2e profile. Without the flag the - * agent-in-the-loop layer (the ask bridge in both sequence arms, and the - * orchestrator's seeded warehouse task) stays unreachable from a test. - * - * Only the e2e TUI host sets the flag, from the `E2E_ASK` env var. No CLI flag - * populates it, so plain `--ci` and `--signup` runs behave exactly as before. - */ -export function shouldDisableAsk( - session: Pick, -): boolean { - return (session.ci || session.signup) && !session.e2eAsk; -} - export function sessionToOptions(session: WizardSession): WizardRunOptions { return { installDir: session.installDir, @@ -87,7 +68,7 @@ export function sessionToOptions(session: WizardSession): WizardRunOptions { export async function bootstrapProgram( session: WizardSession, config: ProgramRun, - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, ): Promise { // 1. Init logging + debug initLogFile(); @@ -115,9 +96,7 @@ export async function bootstrapProgram( // 2. Health check (guarded — skip if TUI already ran it). Only // programs that declare a health-check screen get pre-flight checks; // for everything else the checks never fire and never block. - const hasHealthCheckScreen = programConfig.steps.some( - (s) => s.screenId === 'health-check', - ); + const hasHealthCheckScreen = programConfig.healthCheckDeclared ?? false; if (session.readinessResult) { logToFile( `[agent-runner] readiness pre-computed by TUI: decision=${session.readinessResult.decision}` + @@ -269,17 +248,11 @@ export async function bootstrapProgram( // Park for any interactive step the user must complete AFTER authenticating // but BEFORE the agent runs — e.g. the source-maps project picker, which // needs credentials to scan and writes its choice to frameworkContext that - // the run prompt reads. Generic: await every gated step between auth and run. - const authIndex = programConfig.steps.findIndex((s) => s.screenId === 'auth'); - const runIndex = programConfig.steps.findIndex((s) => s.screenId === 'run'); - if (authIndex !== -1 && runIndex > authIndex) { - for (const step of programConfig.steps.slice(authIndex + 1, runIndex)) { - if (step.gate) { - logToFile(`[agent-runner] awaiting post-auth gate: ${step.id}`); - await getUI().waitForGate(step.id); - logToFile(`[agent-runner] post-auth gate cleared: ${step.id}`); - } - } + // the run prompt reads. The flow layer names them in `postAuthGateIds`. + for (const id of programConfig.postAuthGateIds ?? []) { + logToFile(`[agent-runner] awaiting post-auth gate: ${id}`); + await getUI().waitForGate(id); + logToFile(`[agent-runner] post-auth gate cleared: ${id}`); } // Feature flags. Both arms need these, and the fork decision reads the flags. diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index 4f4292f83..830c43a8d 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -1,120 +1,19 @@ /** - * Shared types for the runner pipeline. + * Shared types for the runner pipeline. The run contract lives in + * `@lib/program-run`; the agent re-exports it for its own modules. */ -import type { - Credentials, - AdditionalFeature, - WizardSession, -} from '@lib/wizard-session'; -import type { PromptContext } from '@lib/agent/agent-prompt'; -import type { PackageManagerDetector } from '@lib/detection/package-manager'; +import type { Credentials } from '@lib/wizard-session'; import type { ApiProject } from '@lib/api'; import type { LLMProvider } from '@posthog/warlock'; -export type { PromptContext, Credentials }; - -/** - * A known `[ABORT] ` case. First matching entry is rendered on - * the error outro; unmatched aborts use a generic fallback. - */ -export interface AbortCase { - match: RegExp; - message: string; - body: string; - docsUrl?: string; - errorCode?: import('@lib/errors').ErrorCode; -} - -/** - * Unified agent run configuration. - * - * Every program provides one of these — either as a static object - * or via a function that builds one from the session. The runner - * assembles the final prompt from `prompt` + `skillId`. - */ -export interface ProgramRun { - /** Analytics label (e.g. 'revenue-analytics-setup', 'nextjs') */ - integrationLabel: string; - /** Skill ID to pre-install. Omit for agent-driven skill discovery. */ - skillId?: string; - /** Additional program-specific prompt instructions. Appended after the default project prompt. */ - customPrompt?: (ctx: PromptContext) => string; - /** Additional MCP servers (e.g. Svelte MCP) */ - additionalMcpServers?: Record; - /** Package manager detector. Defaults to detectNodePackageManagers. */ - detectPackageManager?: PackageManagerDetector; - spinnerMessage: string; - successMessage: string; - estimatedDurationMinutes: number; - reportFile: string; - docsUrl: string; - errorMessage?: string; - additionalFeatureQueue?: readonly AdditionalFeature[]; - /** Known `[ABORT] ` cases this program can render. */ - abortCases?: AbortCase[]; - /** Runs after agent completes, before outro (e.g. env var upload). */ - postRun?: (session: WizardSession, credentials: Credentials) => Promise; - /** Custom outro data. Omit for default built from successMessage/reportFile/docsUrl. */ - buildOutroData?: ( - session: WizardSession, - credentials: Credentials, - ) => WizardSession['outroData']; - /** - * Outro bullets for a sequence that composes its own outro data. - * - * `buildOutroData` is the linear sequence's seam: it hands the program the - * whole outro. The orchestrated sequence cannot, because its message is the - * drain's result — how many steps ran, what was skipped, which conflict the - * review step left. So a program with next steps to offer had nowhere to put - * them there, and the integration's data-source links were built and then - * dropped on every orchestrated run. This hook keeps the message with the - * sequence and the bullets with the program. - * - * `completedSeededTypes` names the runner-seeded task types that finished - * successfully, so a program can leave out a step its own seeded task - * already did — the sequence stays ignorant of what any type means. - */ - buildOutroNextSteps?: ( - session: WizardSession, - credentials: Credentials, - completedSeededTypes: readonly string[], - ) => { heading: string; items: string[] } | undefined; - /** - * Per-run cap on `wizard_ask` invocations. Defaults to 10. The 4th call - * always returns a "batch your questions" error regardless of the cap. - */ - maxQuestions?: number; - /** - * Opt this program's `wizard_ask` overlays into rich link rendering: - * standalone URLs in prompt text become OSC 8 hyperlinks and a lone URL is - * copied to the clipboard, so a long URL can't be broken by the overlay's - * line wrapping. Defaults to false — leave off for flows we don't own. - */ - richLinks?: boolean; - /** - * Per-question `wizard_ask` timeout in milliseconds. Defaults to - * DEFAULT_ASK_TIMEOUT_MS (5 minutes). Raise it for programs whose - * questions send the user off to do slow work (run a build, create a - * key in the browser) before they can answer. - */ - askTimeoutMs?: number; - /** - * Emit a `wizard: step` analytics event on each agent task-list transition - * (in_progress / completed) so this program can build a step-level drop-off - * funnel — including silent steps that ask the user nothing. The step name is - * whatever the agent set on the task. Defaults to off, so no other program's - * analytics change; opt in per program. - */ - trackStepProgress?: boolean; - /** - * Map an agent-authored step label to a stable key, shipped on `wizard: step` as `step_key`. - * The runner knows nothing about any program's steps, so a program that wants its funnel to - * survive the agent rewording a task supplies the mapping itself. Omit it and only the label - * ships, as before. - */ - resolveStepKey?: (stepName: string | undefined) => string | undefined; -} +export type { + PromptContext, + Credentials, + AbortCase, + ProgramRun, + ProgramRunConfig, +} from '@lib/program-run'; /** * Result of the shared bootstrap, consumed by both the linear and the diff --git a/src/lib/agent/runner/switchboard/sequence.ts b/src/lib/agent/runner/switchboard/sequence.ts index e39e228e8..ff8b415a6 100644 --- a/src/lib/agent/runner/switchboard/sequence.ts +++ b/src/lib/agent/runner/switchboard/sequence.ts @@ -13,7 +13,7 @@ import { } from './flags'; import { getHarness, resolveHarness } from './harness'; import type { WizardSession } from '@lib/wizard-session'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramRunConfig } from '@lib/program-run'; import type { ProgramRun, BootstrapResult } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; @@ -32,7 +32,7 @@ export interface SequenceRunner { run( session: WizardSession, config: ProgramRun, - programConfig: ProgramConfig, + programConfig: ProgramRunConfig, boot: BootstrapResult, /** Composed sub-run (integration inside self-driving); linear-only. */ composed: boolean, diff --git a/src/lib/ask-policy.ts b/src/lib/ask-policy.ts new file mode 100644 index 000000000..c0b55652d --- /dev/null +++ b/src/lib/ask-policy.ts @@ -0,0 +1,23 @@ +import type { WizardSession } from '@lib/wizard-session'; + +/** + * Decide whether the `wizard_ask` overlay should be wired for this run. + * Disabled in non-interactive modes (CI, signup) — there's no human to + * answer. Per-program disabling is done by adding WIZARD_ASK_TOOL_NAME to + * the program's `disallowedTools` so the SDK rejects calls outright. + * Extracted so the policy can be unit-tested directly. + * + * `session.e2eAsk` is the one escape hatch. The e2e harness runs a `ci` + * session, but it does have an answerer — the driver loop answers each + * `wizard_ask` batch from the program's e2e profile. Without the flag the + * agent-in-the-loop layer (the ask bridge in both sequence arms, and the + * orchestrator's seeded warehouse task) stays unreachable from a test. + * + * Only the e2e TUI host sets the flag, from the `E2E_ASK` env var. No CLI flag + * populates it, so plain `--ci` and `--signup` runs behave exactly as before. + */ +export function shouldDisableAsk( + session: Pick, +): boolean { + return (session.ci || session.signup) && !session.e2eAsk; +} diff --git a/src/lib/agent/claude-settings.ts b/src/lib/claude-settings.ts similarity index 100% rename from src/lib/agent/claude-settings.ts rename to src/lib/claude-settings.ts diff --git a/src/lib/errors/agent-map.ts b/src/lib/errors/agent-map.ts index d9a46a667..c795f0e5b 100644 --- a/src/lib/errors/agent-map.ts +++ b/src/lib/errors/agent-map.ts @@ -1,4 +1,4 @@ -import { AgentErrorType } from '../agent/signals'; +import { AgentErrorType } from '@lib/agent-signals'; import { ErrorCodes, type ErrorCode } from './codes'; export const AGENT_ERROR_CODE: Record = { diff --git a/src/lib/middleware/benchmarks/cost-tracker.ts b/src/lib/middleware/benchmarks/cost-tracker.ts index 7e33ba0f4..b65b4f5a0 100644 --- a/src/lib/middleware/benchmarks/cost-tracker.ts +++ b/src/lib/middleware/benchmarks/cost-tracker.ts @@ -3,7 +3,7 @@ import type { MiddlewareContext, MiddlewareStore, } from '@lib/middleware/types'; -import { computeTokenCostUsd } from '@lib/agent/token-pricing'; +import { computeTokenCostUsd } from '@lib/token-pricing'; import type { TokenData } from './token-tracker'; import type { CacheData } from './cache-tracker'; diff --git a/src/lib/program-run.ts b/src/lib/program-run.ts new file mode 100644 index 000000000..5296e6a6b --- /dev/null +++ b/src/lib/program-run.ts @@ -0,0 +1,250 @@ +/** + * The run contract between the store and the agent. Programs describe what to + * run with these types; the agent executes one run and never reads program + * steps. Both surfaces import from here. + */ + +import type { + Credentials, + AdditionalFeature, + WizardSession, + TaskNotice, +} from '@lib/wizard-session'; +import type { PackageManagerDetector } from '@lib/detection/package-manager'; +import type { HostResolution } from '@lib/host-resolution'; + +export type { Credentials }; + +/** + * Values available to prompt builders after OAuth completes. + */ +export interface PromptContext { + projectId: number; + projectApiKey: string; + host: HostResolution; + /** Set when skillId was provided and the skill was installed successfully. */ + skillPath?: string; + /** + * Org-level AI consent (`is_ai_data_processing_approved`) read from the + * `/api/users/@me/` payload at auth time. `null` = unknown (older orgs, + * or the user fetch failed). Lets prompts pre-resolve consent state so + * agents only ask the user when it is actually off or unknown. + */ + orgAiDataProcessingApproved?: boolean | null; + /** + * Team product opt-ins from the `/api/projects/:id/` payload at auth + * time. Project-level truth for "is this product enabled" — products + * can be instrumented from other repos or the snippet, so repo-local + * evidence must never rule them out. `null` field = unknown. + */ + teamProductOptIns?: { + sessionReplay?: boolean | null; + exceptionAutocapture?: boolean | null; + surveys?: boolean | null; + } | null; +} + +/** + * A known `[ABORT] ` case. First matching entry is rendered on + * the error outro; unmatched aborts use a generic fallback. + */ +export interface AbortCase { + match: RegExp; + message: string; + body: string; + docsUrl?: string; + errorCode?: import('@lib/errors').ErrorCode; +} + +/** + * Unified agent run configuration. + * + * Every program provides one of these — either as a static object + * or via a function that builds one from the session. The runner + * assembles the final prompt from `prompt` + `skillId`. + */ +export interface ProgramRun { + /** Analytics label (e.g. 'revenue-analytics-setup', 'nextjs') */ + integrationLabel: string; + /** Skill ID to pre-install. Omit for agent-driven skill discovery. */ + skillId?: string; + /** Additional program-specific prompt instructions. Appended after the default project prompt. */ + customPrompt?: (ctx: PromptContext) => string; + /** Additional MCP servers (e.g. Svelte MCP) */ + additionalMcpServers?: Record; + /** Package manager detector. Defaults to detectNodePackageManagers. */ + detectPackageManager?: PackageManagerDetector; + spinnerMessage: string; + successMessage: string; + estimatedDurationMinutes: number; + reportFile: string; + docsUrl: string; + errorMessage?: string; + additionalFeatureQueue?: readonly AdditionalFeature[]; + /** Known `[ABORT] ` cases this program can render. */ + abortCases?: AbortCase[]; + /** Runs after agent completes, before outro (e.g. env var upload). */ + postRun?: (session: WizardSession, credentials: Credentials) => Promise; + /** Custom outro data. Omit for default built from successMessage/reportFile/docsUrl. */ + buildOutroData?: ( + session: WizardSession, + credentials: Credentials, + ) => WizardSession['outroData']; + /** + * Outro bullets for a sequence that composes its own outro data. + * + * `buildOutroData` is the linear sequence's seam: it hands the program the + * whole outro. The orchestrated sequence cannot, because its message is the + * drain's result — how many steps ran, what was skipped, which conflict the + * review step left. So a program with next steps to offer had nowhere to put + * them there, and the integration's data-source links were built and then + * dropped on every orchestrated run. This hook keeps the message with the + * sequence and the bullets with the program. + * + * `completedSeededTypes` names the runner-seeded task types that finished + * successfully, so a program can leave out a step its own seeded task + * already did — the sequence stays ignorant of what any type means. + */ + buildOutroNextSteps?: ( + session: WizardSession, + credentials: Credentials, + completedSeededTypes: readonly string[], + ) => { heading: string; items: string[] } | undefined; + /** + * Per-run cap on `wizard_ask` invocations. Defaults to 10. The 4th call + * always returns a "batch your questions" error regardless of the cap. + */ + maxQuestions?: number; + /** + * Opt this program's `wizard_ask` overlays into rich link rendering: + * standalone URLs in prompt text become OSC 8 hyperlinks and a lone URL is + * copied to the clipboard, so a long URL can't be broken by the overlay's + * line wrapping. Defaults to false — leave off for flows we don't own. + */ + richLinks?: boolean; + /** + * Per-question `wizard_ask` timeout in milliseconds. Defaults to + * DEFAULT_ASK_TIMEOUT_MS (5 minutes). Raise it for programs whose + * questions send the user off to do slow work (run a build, create a + * key in the browser) before they can answer. + */ + askTimeoutMs?: number; + /** + * Emit a `wizard: step` analytics event on each agent task-list transition + * (in_progress / completed) so this program can build a step-level drop-off + * funnel — including silent steps that ask the user nothing. The step name is + * whatever the agent set on the task. Defaults to off, so no other program's + * analytics change; opt in per program. + */ + trackStepProgress?: boolean; + /** + * Map an agent-authored step label to a stable key, shipped on `wizard: step` as `step_key`. + * The runner knows nothing about any program's steps, so a program that wants its funnel to + * survive the agent rewording a task supplies the mapping itself. Omit it and only the label + * ships, as before. + */ + resolveStepKey?: (stepName: string | undefined) => string | undefined; +} + +/** A task the runner queues itself, before the planner runs. */ +export interface SeedTask { + type: string; + label?: string; + inputs?: Record; + /** + * Shown before the run starts, letting the user decline the task. The + * program owns the words — the runner and the modal only carry them. A + * task without one is queued silently. + */ + notice?: TaskNotice; +} + +/** + * What the agent receives for one run: the fields of a program the runner + * reads. `ProgramConfig` extends this with steps and CLI shape, which the + * agent never sees. + */ +export interface ProgramRunConfig { + /** Unique program id — matches the Program enum value */ + id: string; + /** + * Content-mill flow the orchestrator loads its agent prompts + step-skills + * from (`agents//` and `skills//`). Defaults to `id`; set it when + * the content-mill flow name diverges from the program id. + */ + agentFlow?: string; + /** + * Whether this program's agent run requires third-party AI services. + * + * When true (the default), the wizard checks + * `apiUser.organization.is_ai_data_processing_approved` after auth and + * renders `AiOptInRequiredScreen` if the org has not opted in. Matches + * Max's strict reading: only literal `true` proceeds. + * + * Opt out (set to `false`) for programs that don't run the agent — + * doctor, mcp install/remove/tutorial, source-map upload. The safe + * default is `true` so future programs gate by declaration. + */ + requiresAi?: boolean; + /** + * Context-mill skill ID this program installs and runs. When present, + * bin.ts seeds `session.skillId` with this value before the TUI renders + * so intro screens can resolve skill metadata without waiting for the + * agent run. + */ + skillId?: string; + /** Agent run config. Static object or async function for dynamic config. */ + run?: ProgramRun | ((session: WizardSession) => Promise); + /** + * Tasks the orchestrator queues itself, before the planner runs, from what + * the wizard detected. Their types are marked `runnerSeeded: true` in the + * agent prompt, so the planner never sees them: whether such a task runs is + * decided here, in code, not by a model that could invent it or forget it. + * Return an empty list to queue none. + */ + seedTasks?: (session: WizardSession) => SeedTask[]; + /** + * Path (relative to installDir) of the report file the program writes. + * Mirrors `run.reportFile` but lifted to the top level so UI screens can + * read it synchronously without resolving a deferred `run` function. + */ + reportFile?: string; + /** + * Agent-authored event-plan artifact to mirror into the wizard session. + * Relative to `session.installDir`. Programs that do not produce an event + * plan leave this unset, so generic runner machinery does not inspect a + * stale or unrelated `.posthog-events.json` file. + */ + eventPlanFile?: string; + /** Audit ledger to mirror into the session, relative to `installDir`. */ + auditLedgerFile?: string; + /** + * Channel the task stream publishes this run under, when it differs from the + * program id. A family leaf runs on the generic skill program, so without + * this every `wizard audit ` would report as `agent-skill`. + */ + streamWorkflowId?: string; + /** + * Extra tool names added on top of BASE_ALLOWED_TOOLS for this program's + * agent run. Use for tools that only this program needs. + */ + allowedTools?: readonly string[]; + /** + * Tool names removed from BASE_ALLOWED_TOOLS for this program's agent + * run. Use to forbid a base tool — e.g. `['Agent']` to block subagent + * dispatch in a program whose steps are explicitly single-agent. + */ + disallowedTools?: readonly string[]; + /** + * Gated step ids between the auth and run steps, awaited after login and + * before the agent starts. The flow layer computes them from the steps + * (`runConfigFor`); the agent only awaits them. + */ + postAuthGateIds?: readonly string[]; + /** + * True when the flow declares a health check step, so bootstrap runs the + * service pre-flight. Computed by `runConfigFor`; other programs never + * probe and never block. + */ + healthCheckDeclared?: boolean; +} diff --git a/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap b/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap index 764f5263a..3e82b4455 100644 --- a/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap +++ b/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap @@ -1,5 +1,30 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`health check declared per program > matches the golden 1`] = ` +{ + "agent-skill": true, + "ai-observability": true, + "audit": true, + "error-tracking": true, + "error-tracking-upload-source-maps": false, + "events-audit": true, + "mcp-add": false, + "mcp-analytics": true, + "mcp-remove": false, + "mcp-tutorial": false, + "metrics": true, + "migration": true, + "posthog-doctor": true, + "posthog-integration": true, + "replay-vision": true, + "revenue-analytics-setup": true, + "self-driving": true, + "slack": false, + "warehouse-source": false, + "web-analytics-doctor": true, +} +`; + exports[`post-auth gate ids per program > match the golden 1`] = ` { "agent-skill": [], diff --git a/src/lib/programs/__tests__/agent-skill.test.ts b/src/lib/programs/__tests__/agent-skill.test.ts index 132586e1d..f3df4a406 100644 --- a/src/lib/programs/__tests__/agent-skill.test.ts +++ b/src/lib/programs/__tests__/agent-skill.test.ts @@ -3,7 +3,7 @@ import { AGENT_SKILL_STEPS, type SkillProgramOptions, } from '@lib/programs/agent-skill/index'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import { buildSession, RunPhase } from '@lib/wizard-session'; import { HostResolution } from '@lib/host-resolution'; diff --git a/src/lib/programs/__tests__/error-tracking.test.ts b/src/lib/programs/__tests__/error-tracking.test.ts index 7cf0561d8..ec048f9fb 100644 --- a/src/lib/programs/__tests__/error-tracking.test.ts +++ b/src/lib/programs/__tests__/error-tracking.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -import type { ProgramRun } from '@lib/agent/runner/shared/types'; +import type { ProgramRun } from '@lib/program-run'; import { Integration } from '@lib/constants'; import type { AgenticDetectionReport } from '@lib/detection/agentic'; import { detectFramework } from '@lib/detection/index'; diff --git a/src/lib/programs/__tests__/metrics-program.test.ts b/src/lib/programs/__tests__/metrics-program.test.ts index ae5936408..3a7eba662 100644 --- a/src/lib/programs/__tests__/metrics-program.test.ts +++ b/src/lib/programs/__tests__/metrics-program.test.ts @@ -1,7 +1,7 @@ import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/index'; import { getProgramConfig, Program } from '@lib/programs/program-registry'; import { metricsConfig } from '@lib/programs/metrics/index'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import { metricsCommand } from '../../../commands/metrics'; diff --git a/src/lib/programs/__tests__/post-auth-gates.test.ts b/src/lib/programs/__tests__/post-auth-gates.test.ts index 01ad25efd..15caa52a2 100644 --- a/src/lib/programs/__tests__/post-auth-gates.test.ts +++ b/src/lib/programs/__tests__/post-auth-gates.test.ts @@ -1,26 +1,24 @@ /** * Golden of the post-auth gate ids the agent runner awaits per program - * (the walk at runner/shared/bootstrap.ts between the `auth` and `run` steps). + * (`postAuthGateIdsFor`, the walk between the `auth` and `run` steps). */ import { PROGRAM_REGISTRY } from '../program-registry'; - -function legacyPostAuthGateIds( - steps: (typeof PROGRAM_REGISTRY)[number]['steps'], -): string[] { - const authIndex = steps.findIndex((s) => s.screenId === 'auth'); - const runIndex = steps.findIndex((s) => s.screenId === 'run'); - if (authIndex === -1 || runIndex <= authIndex) return []; - return steps - .slice(authIndex + 1, runIndex) - .filter((s) => s.gate) - .map((s) => s.id); -} +import { postAuthGateIdsFor, runConfigFor } from '../run-config'; describe('post-auth gate ids per program', () => { it('match the golden', () => { const gates = Object.fromEntries( - PROGRAM_REGISTRY.map((c) => [c.id, legacyPostAuthGateIds(c.steps)]), + PROGRAM_REGISTRY.map((c) => [c.id, postAuthGateIdsFor(c.steps)]), ); expect(gates).toMatchSnapshot(); }); }); + +describe('health check declared per program', () => { + it('matches the golden', () => { + const declared = Object.fromEntries( + PROGRAM_REGISTRY.map((c) => [c.id, runConfigFor(c).healthCheckDeclared]), + ); + expect(declared).toMatchSnapshot(); + }); +}); diff --git a/src/lib/programs/__tests__/self-driving-prompt.test.ts b/src/lib/programs/__tests__/self-driving-prompt.test.ts index d23e7c1bc..36d850cb9 100644 --- a/src/lib/programs/__tests__/self-driving-prompt.test.ts +++ b/src/lib/programs/__tests__/self-driving-prompt.test.ts @@ -1,5 +1,5 @@ import { buildSelfDrivingPrompt } from '@lib/programs/self-driving/prompt'; -import type { PromptContext } from '@lib/agent/agent-runner'; +import type { PromptContext } from '@lib/program-run'; import { HostResolution } from '@lib/host-resolution'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/agent-skill/index.ts b/src/lib/programs/agent-skill/index.ts index dbd207ae0..8612e71a8 100644 --- a/src/lib/programs/agent-skill/index.ts +++ b/src/lib/programs/agent-skill/index.ts @@ -20,7 +20,7 @@ */ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun, AbortCase } from '@lib/agent/agent-runner'; +import type { ProgramRun, AbortCase } from '@lib/program-run'; import { AGENT_SKILL_STEPS } from './steps.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/audit/detect.ts b/src/lib/programs/audit/detect.ts index 2cfa299a5..4312f79f1 100644 --- a/src/lib/programs/audit/detect.ts +++ b/src/lib/programs/audit/detect.ts @@ -1,4 +1,4 @@ -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { ErrorCodes } from '@lib/errors'; /** `[ABORT] ` cases the audit skill can emit. Reason strings are diff --git a/src/lib/programs/audit/index.ts b/src/lib/programs/audit/index.ts index fc5f60297..e0847a05d 100644 --- a/src/lib/programs/audit/index.ts +++ b/src/lib/programs/audit/index.ts @@ -3,7 +3,7 @@ import { createSkillProgram, } from '@lib/programs/agent-skill/index'; import type { ProgramStep, ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; diff --git a/src/lib/programs/error-tracking-upload-source-maps/detect.ts b/src/lib/programs/error-tracking-upload-source-maps/detect.ts index 2d81b0061..5fa9e7ddd 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/detect.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/detect.ts @@ -17,7 +17,7 @@ import { safeReadFile, } from '@utils/bounded-fs'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { ErrorCodes } from '@lib/errors'; /** diff --git a/src/lib/programs/error-tracking-upload-source-maps/index.ts b/src/lib/programs/error-tracking-upload-source-maps/index.ts index c79f245e2..2b77c280e 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/index.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { ERROR_TRACKING_UPLOAD_SOURCE_MAPS_PROGRAM } from './steps.js'; diff --git a/src/lib/programs/error-tracking-upload-source-maps/prompt.ts b/src/lib/programs/error-tracking-upload-source-maps/prompt.ts index 3d19efceb..ed31336c6 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/prompt.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/prompt.ts @@ -1,4 +1,4 @@ -import { AgentSignals } from '@lib/agent/agent-interface'; +import { AgentSignals } from '@lib/agent-signals'; import type { SkillVariant } from './detect.js'; export type SourceMapsUploadPromptParams = { diff --git a/src/lib/programs/error-tracking/index.ts b/src/lib/programs/error-tracking/index.ts index 2fd6a7cb1..82aa9359f 100644 --- a/src/lib/programs/error-tracking/index.ts +++ b/src/lib/programs/error-tracking/index.ts @@ -2,7 +2,7 @@ import { Integration } from '@lib/constants'; import { detectFramework } from '@lib/detection/index'; import { scopeInstallDirToProject } from '@lib/detection/project-scope'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import type { ProgramRun } from '@lib/agent/runner/shared/types'; +import type { ProgramRun } from '@lib/program-run'; import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/steps'; import { getContentBlocks } from '@lib/programs/error-tracking/content/index'; import { getTips } from '@lib/programs/error-tracking/content/tips'; diff --git a/src/lib/programs/events-audit/index.ts b/src/lib/programs/events-audit/index.ts index 080e59d69..ae5d1dd9f 100644 --- a/src/lib/programs/events-audit/index.ts +++ b/src/lib/programs/events-audit/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { SPINNER_MESSAGE } from '@lib/framework-config'; diff --git a/src/lib/programs/mcp-analytics/index.ts b/src/lib/programs/mcp-analytics/index.ts index f9199d76e..c4f16857e 100644 --- a/src/lib/programs/mcp-analytics/index.ts +++ b/src/lib/programs/mcp-analytics/index.ts @@ -1,4 +1,4 @@ -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { ErrorCodes } from '@lib/errors'; import { createSkillProgram } from '@lib/programs/agent-skill/index'; diff --git a/src/lib/programs/migration/index.ts b/src/lib/programs/migration/index.ts index 27cf0b3d0..63bcdf816 100644 --- a/src/lib/programs/migration/index.ts +++ b/src/lib/programs/migration/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import { MIGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index c3ac0b913..274ca6aa3 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -1,9 +1,9 @@ import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; -import { runAgent, type ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import type { WizardSession } from '@lib/wizard-session'; import { mayReportScanResults, OutroKind, RunPhase } from '@lib/wizard-session'; -import { AgentSignals } from '@lib/agent/agent-interface'; +import { AgentSignals } from '@lib/agent-signals'; import { DEFAULT_PACKAGE_INSTALLATION, SPINNER_MESSAGE, @@ -21,7 +21,7 @@ import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; import type { HostResolution } from '@lib/host-resolution'; import { getDetectedWarehouseSources } from '@lib/programs/warehouse-source/detect'; -import { shouldDisableAsk } from '@lib/agent/runner/shared/bootstrap'; +import { shouldDisableAsk } from '@lib/ask-policy'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; import { buildCodingAgentPrompt } from './handoff.js'; @@ -477,8 +477,7 @@ export const integrationRunStep: ProgramStep = { 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) => - runAgent(posthogIntegrationConfig, session, { composed: true }), + run: { programId: 'posthog-integration' }, isComplete: (session) => session.runPhase === RunPhase.Completed || session.runPhase === RunPhase.Error, diff --git a/src/lib/programs/program-step.ts b/src/lib/programs/program-step.ts index 9388bee79..dc670f010 100644 --- a/src/lib/programs/program-step.ts +++ b/src/lib/programs/program-step.ts @@ -1,10 +1,6 @@ -import type { - WizardSession, - DiscoveredFeature, - TaskNotice, -} from '@lib/wizard-session'; +import type { WizardSession, DiscoveredFeature } from '@lib/wizard-session'; import type { WizardReadinessResult } from '@lib/health-checks/readiness'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRunConfig } from '@lib/program-run'; import type { Integration } from '@lib/constants'; import type { FrameworkConfig } from '@lib/framework-config'; import type { ContentBlock } from '@ui/tui/primitives/index'; @@ -76,13 +72,12 @@ export interface ProgramStep { screenId?: string; /** - * For a run step (`screenId: 'run'`): runs this step's own agent. A program - * exports a self-contained run step and another imports it into its step list - * — e.g. posthog-integration exports a run step that runs its agent, and - * self-driving imports it before its own run step. Omit to run the host - * program's own agent (`config.run`). + * For a run step (`screenId: 'run'`): the program whose agent this step runs, + * composed into the host program's step list (self-driving runs the + * integration's agent before its own). The runner executes it in the step's + * `targetDir` after `onRunPrep`. Omit to run the host program's own agent. */ - run?: (session: WizardSession) => Promise; + run?: { programId: ProgramId }; /** * For a run step: prepare a derived session before its agent runs — e.g. @@ -199,7 +194,7 @@ export interface ProgramCliSurface { * Each program directory exports one of these. The system uses it * for CLI registration, sequence/step wiring, and skill bootstrap. */ -export interface ProgramConfig { +export interface ProgramConfig extends ProgramRunConfig { /** CLI command name (e.g. 'revenue-analytics'). Omit for the default program. */ command?: string; /** @@ -211,38 +206,8 @@ export interface ProgramConfig { parentCommand?: string; /** CLI description shown in --help */ description: string; - /** Unique program id — matches the Program enum value */ - id: string; - /** - * Content-mill flow the orchestrator loads its agent prompts + step-skills - * from (`agents//` and `skills//`). Defaults to `id`; set it when - * the content-mill flow name diverges from the program id. - */ - agentFlow?: string; - /** - * Whether this program's agent run requires third-party AI services. - * - * When true (the default), the wizard checks - * `apiUser.organization.is_ai_data_processing_approved` after auth and - * renders `AiOptInRequiredScreen` if the org has not opted in. Matches - * Max's strict reading: only literal `true` proceeds. - * - * Opt out (set to `false`) for programs that don't run the agent — - * doctor, mcp install/remove/tutorial, source-map upload. The safe - * default is `true` so future programs gate by declaration. - */ - requiresAi?: boolean; - /** - * Context-mill skill ID this program installs and runs. When present, - * bin.ts seeds `session.skillId` with this value before the TUI renders - * so intro screens can resolve skill metadata without waiting for the - * agent run. - */ - skillId?: string; /** The ordered step list */ steps: ProgramStep[]; - /** Agent run config. Static object or async function for dynamic config. */ - run?: ProgramRun | ((session: WizardSession) => Promise); /** * CI-mode pre-run strategy. When set, runWizardCI awaits this after building * the ci:true session and before the agent runs, instead of walking step @@ -250,47 +215,8 @@ export interface ProgramConfig { * detection) that the TUI performs via step onReady callbacks. */ ciPreRun?: (session: WizardSession) => Promise; - /** - * Tasks the orchestrator queues itself, before the planner runs, from what - * the wizard detected. Their types are marked `runnerSeeded: true` in the - * agent prompt, so the planner never sees them: whether such a task runs is - * decided here, in code, not by a model that could invent it or forget it. - * Return an empty list to queue none. - */ - seedTasks?: (session: WizardSession) => Array<{ - type: string; - label?: string; - inputs?: Record; - /** - * Shown before the run starts, letting the user decline the task. The - * program owns the words — the runner and the modal only carry them. A - * task without one is queued silently. - */ - notice?: TaskNotice; - }>; /** Prerequisites: other program ids that must have run first */ requires?: string[]; - /** - * Path (relative to installDir) of the report file the program writes. - * Mirrors `run.reportFile` but lifted to the top level so UI screens can - * read it synchronously without resolving a deferred `run` function. - */ - reportFile?: string; - /** - * Agent-authored event-plan artifact to mirror into the wizard session. - * Relative to `session.installDir`. Programs that do not produce an event - * plan leave this unset, so generic runner machinery does not inspect a - * stale or unrelated `.posthog-events.json` file. - */ - eventPlanFile?: string; - /** Audit ledger to mirror into the session, relative to `installDir`. */ - auditLedgerFile?: string; - /** - * Channel the task stream publishes this run under, when it differs from the - * program id. A family leaf runs on the generic skill program, so without - * this every `wizard audit ` would report as `agent-skill`. - */ - streamWorkflowId?: string; /** * LearnCard deck rendered in the shared `RunScreen` while the agent * runs. Lives at `/content/index.tsx` by convention. @@ -321,17 +247,6 @@ export interface ProgramConfig { * 'migrate-statsig'`). */ mapCliOptions?: (argv: Record) => Record; - /** - * Extra tool names added on top of BASE_ALLOWED_TOOLS for this program's - * agent run. Use for tools that only this program needs. - */ - allowedTools?: readonly string[]; - /** - * Tool names removed from BASE_ALLOWED_TOOLS for this program's agent - * run. Use to forbid a base tool — e.g. `['Agent']` to block subagent - * dispatch in a program whose steps are explicitly single-agent. - */ - disallowedTools?: readonly string[]; /** * Declares this program's place in the wizard CLI surface. See * `ProgramCliSurface` for semantics. diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index 046f76e25..0bb2b0b2f 100644 --- a/src/lib/programs/replay-vision/index.ts +++ b/src/lib/programs/replay-vision/index.ts @@ -1,4 +1,4 @@ -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { Integration } from '@lib/constants'; import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; import { scopeInstallDirToProject } from '@lib/detection/project-scope'; diff --git a/src/lib/programs/revenue-analytics/detect.ts b/src/lib/programs/revenue-analytics/detect.ts index de75eae0b..4fff50d25 100644 --- a/src/lib/programs/revenue-analytics/detect.ts +++ b/src/lib/programs/revenue-analytics/detect.ts @@ -7,7 +7,7 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { findPackageJsons } from '@lib/programs/shared/package-scanning'; export { diff --git a/src/lib/programs/run-config.ts b/src/lib/programs/run-config.ts new file mode 100644 index 000000000..0ddb1c8f5 --- /dev/null +++ b/src/lib/programs/run-config.ts @@ -0,0 +1,24 @@ +import type { ProgramRunConfig } from '@lib/program-run'; +import type { ProgramConfig, ProgramStep } from './program-step.js'; + +/** Gated step ids between the `auth` and the first `run` step, in order. */ +export function postAuthGateIdsFor(steps: readonly ProgramStep[]): string[] { + const authIndex = steps.findIndex((s) => s.screenId === 'auth'); + const runIndex = steps.findIndex((s) => s.screenId === 'run'); + if (authIndex === -1 || runIndex <= authIndex) return []; + return steps + .slice(authIndex + 1, runIndex) + .filter((s) => s.gate) + .map((s) => s.id); +} + +/** The run contract the agent receives for a program. */ +export function runConfigFor(config: ProgramConfig): ProgramRunConfig { + return { + ...config, + postAuthGateIds: postAuthGateIdsFor(config.steps), + healthCheckDeclared: config.steps.some( + (s) => s.screenId === 'health-check', + ), + }; +} diff --git a/src/lib/programs/self-driving/detect.ts b/src/lib/programs/self-driving/detect.ts index d9c74f055..94e30c33e 100644 --- a/src/lib/programs/self-driving/detect.ts +++ b/src/lib/programs/self-driving/detect.ts @@ -29,7 +29,7 @@ import { import { join } from 'path'; import { analytics } from '@utils/analytics'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { ErrorCodes } from '@lib/errors'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/self-driving/index.ts b/src/lib/programs/self-driving/index.ts index b507cc18e..8878137f3 100644 --- a/src/lib/programs/self-driving/index.ts +++ b/src/lib/programs/self-driving/index.ts @@ -1,7 +1,7 @@ import { join } from 'path'; import { access, rm } from 'node:fs/promises'; import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import { OutroKind, type WizardSession } from '@lib/wizard-session'; import { createSkillProgram } from '../agent-skill/index.js'; import { SELF_DRIVING_PROGRAM } from './steps.js'; diff --git a/src/lib/programs/self-driving/prompt.ts b/src/lib/programs/self-driving/prompt.ts index 5bb0a6af2..07033c4d0 100644 --- a/src/lib/programs/self-driving/prompt.ts +++ b/src/lib/programs/self-driving/prompt.ts @@ -1,5 +1,5 @@ -import { AgentSignals } from '@lib/agent/agent-interface'; -import type { PromptContext } from '@lib/agent/agent-runner'; +import { AgentSignals } from '@lib/agent-signals'; +import type { PromptContext } from '@lib/program-run'; import type { DetectedSource } from '@lib/warehouse-sources/types'; /** diff --git a/src/lib/programs/warehouse-source/detect.ts b/src/lib/programs/warehouse-source/detect.ts index 9e97b7d0c..d6c6bc490 100644 --- a/src/lib/programs/warehouse-source/detect.ts +++ b/src/lib/programs/warehouse-source/detect.ts @@ -9,7 +9,7 @@ import { existsSync, statSync } from 'fs'; import { analytics } from '@utils/analytics'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/warehouse-source/index.ts b/src/lib/programs/warehouse-source/index.ts index 059e240a0..94836a4c7 100644 --- a/src/lib/programs/warehouse-source/index.ts +++ b/src/lib/programs/warehouse-source/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { LONGER_ASK_TIMEOUT_MS } from '@lib/wizard-ask-bridge'; import { WAREHOUSE_SOURCE_PROGRAM } from './steps.js'; diff --git a/src/lib/programs/web-analytics-doctor/detect.ts b/src/lib/programs/web-analytics-doctor/detect.ts index 749f4e75d..10f037282 100644 --- a/src/lib/programs/web-analytics-doctor/detect.ts +++ b/src/lib/programs/web-analytics-doctor/detect.ts @@ -1,6 +1,6 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/program-run'; import { ErrorCodes } from '@lib/errors'; import { findPackageJsons } from '@lib/programs/shared/package-scanning'; diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 7230c3ec0..6f0226acd 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -8,6 +8,7 @@ import type { CloudRegion } from '@utils/types'; import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import type { ProgramConfig } from '@lib/programs/program-step'; +import { runConfigFor } from '@lib/programs/run-config'; import { getAuditChecks } from '@lib/programs/audit/types'; import { analytics } from '@utils/analytics'; import { resolveNoTelemetry } from './resolve-no-telemetry'; @@ -336,7 +337,7 @@ export function runNonInteractive( } const { runAgent } = await import('@lib/agent/agent-runner'); - await runAgent(config, session); + await runAgent(runConfigFor(config), session); await settleStream(RunPhase.Completed); } catch (error) { const errorMessage = diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index 1ee2e7120..68124a9e4 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -3,6 +3,7 @@ import { logToFile, getLogFilePath } from '@utils/debug'; import { runAgent } from '@lib/agent/agent-runner'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { getProgramConfig } from '@lib/programs/program-registry'; +import { runConfigFor } from '@lib/programs/run-config'; import { getAuditChecks } from '@lib/programs/audit/types'; import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; import type { ProgramConfig } from '@lib/programs/program-step'; @@ -57,10 +58,17 @@ async function advanceStep( await authenticate(store.session, config.id); maybeStampAiSdkDetected(store.session); } else if (step.run) { - await step.run(await prepareRunSession(step, store.session)); + await runAgent( + runConfigFor(getProgramConfig(step.run.programId)), + await prepareRunSession(step, store.session), + { composed: true }, + ); store.completeRunStep(step.id); } else if (step.screenId === 'run') { - await runAgent(config, await prepareRunSession(step, store.session)); + await runAgent( + runConfigFor(config), + await prepareRunSession(step, store.session), + ); } else if (step.isComplete) { await store.waitUntil(step.isComplete); } @@ -262,7 +270,7 @@ export function runWizard( }); } else { try { - await runAgent(config, activeTui.store.session); + await runAgent(runConfigFor(config), activeTui.store.session); } catch (error) { // The run threw before its own error handling rendered an outro. // Show the handoff screen and let the user's agent take over. diff --git a/src/lib/agent/token-pricing.ts b/src/lib/token-pricing.ts similarity index 100% rename from src/lib/agent/token-pricing.ts rename to src/lib/token-pricing.ts diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 2fc028d59..bee8c8de9 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -14,7 +14,7 @@ import { POSTHOG_LOCAL_URL, resolveLocalDev } from './local-dev'; import type { Harness, Integration, Sequence } from './constants'; import type { FrameworkConfig } from './framework-config'; import type { WizardReadinessResult } from './health-checks/readiness'; -import type { SettingsConflict } from './agent/claude-settings'; +import type { SettingsConflict } from './claude-settings'; import type { ApiUser, ApiProject } from './api'; import type { HostResolution } from './host-resolution'; diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index 30ff56dac..a0b880d37 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -11,7 +11,7 @@ import { type AuthErrorDetail, type TokenUsageDelta, } from './wizard-ui'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; import type { ApiUser } from '@lib/api'; import { OAUTH_TIMEOUT_MS } from '@lib/constants'; import { @@ -28,6 +28,7 @@ import type { } from '@lib/wizard-session'; export class LoggingUI implements WizardUI { + readonly interactive = false; intro(message: string): void { console.log(`┌ ${message}`); } diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts index 8977738e7..e40535eca 100644 --- a/src/ui/tui/__tests__/store-invariants.test.ts +++ b/src/ui/tui/__tests__/store-invariants.test.ts @@ -32,7 +32,7 @@ import { Integration } from '@lib/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { analytics } from '@utils/analytics'; import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; vi.mock('../../../utils/analytics.js', () => ({ analytics: { diff --git a/src/ui/tui/components/TokenCostHud.tsx b/src/ui/tui/components/TokenCostHud.tsx index 83fb1085d..d0b8e0ca3 100644 --- a/src/ui/tui/components/TokenCostHud.tsx +++ b/src/ui/tui/components/TokenCostHud.tsx @@ -17,7 +17,7 @@ import { Box, Text } from 'ink'; import { Colors } from '@ui/tui/styles'; import { totalTokenCount, type TokenUsageSnapshot } from '@ui/tui/store'; -import { formatTokenCount, formatCostUsd } from '@lib/agent/token-pricing'; +import { formatTokenCount, formatCostUsd } from '@lib/token-pricing'; /** Self-documents the hidden shortcut once the panel is showing. */ const HINT_TEXT = 'Ctrl+T to hide'; diff --git a/src/ui/tui/exit-line.ts b/src/ui/tui/exit-line.ts index 7db085ead..000402a06 100644 --- a/src/ui/tui/exit-line.ts +++ b/src/ui/tui/exit-line.ts @@ -16,7 +16,7 @@ import { totalTokenCount, type WizardStore } from './store.js'; import { OutroKind } from '@lib/wizard-session'; import { isRunFailure, MINT_FAILURE_CONTACT } from '@ui/mint-failure'; -import { formatTokenCount, formatCostUsd } from '@lib/agent/token-pricing'; +import { formatTokenCount, formatCostUsd } from '@lib/token-pricing'; import { getLogFilePath } from '@utils/debug'; const RESET_ATTRS = '\x1b[0m'; diff --git a/src/ui/tui/ink-ui.ts b/src/ui/tui/ink-ui.ts index 60c9f27da..4e05831a9 100644 --- a/src/ui/tui/ink-ui.ts +++ b/src/ui/tui/ink-ui.ts @@ -13,7 +13,7 @@ import type { TokenUsageDelta, } from '@ui/wizard-ui'; import type { WizardStore } from './store.js'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; import type { WizardReadinessResult } from '@lib/health-checks/readiness'; import type { ApiUser } from '@lib/api'; import type { @@ -33,6 +33,7 @@ function stripAnsi(s: string): string { } export class InkUI implements WizardUI { + readonly interactive = true; constructor(private store: WizardStore) {} intro(message: string): void { diff --git a/src/ui/tui/screens/ManagedSettingsScreen.tsx b/src/ui/tui/screens/ManagedSettingsScreen.tsx index dcb184aa6..a7911b58d 100644 --- a/src/ui/tui/screens/ManagedSettingsScreen.tsx +++ b/src/ui/tui/screens/ManagedSettingsScreen.tsx @@ -14,7 +14,7 @@ import { useEffect, useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { ConfirmationInput, ModalOverlay } from '@ui/tui/primitives/index'; import { Icons } from '@ui/tui/styles'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; import { analytics } from '@utils/analytics'; function sourceLabel(source: SettingsConflict['source']): string { diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index 4e98e5d0b..f43223d51 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -35,7 +35,7 @@ import { buildSession, type TaskNotice, } from '@lib/wizard-session'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; import { WizardReadiness, getBlockingServiceKeys, @@ -59,7 +59,7 @@ import { withAiOptInGate } from '@lib/programs/ai-opt-in-gate'; import { reportWarehouseSourcesDetected } from '@lib/programs/posthog-integration/detect'; import { EXPANDED_COUNT } from '@ui/tui/constants'; import { IS_DEV } from '@lib/constants'; -import { computeTokenCostUsd } from '@lib/agent/token-pricing'; +import { computeTokenCostUsd } from '@lib/token-pricing'; export { TaskStatus, ScreenId, Overlay, Program, RunPhase, McpOutcome }; export type { ScreenName, OutroData, WizardSession, ProgramId }; diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index 38ad29235..ddc43b016 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -8,7 +8,7 @@ * Session-mutating methods trigger reactive screen resolution in the TUI. */ -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import type { SettingsConflict } from '@lib/claude-settings'; import type { WizardReadinessResult } from '@lib/health-checks/readiness'; import type { ApiUser } from '@lib/api'; import type { Credentials, TaskNotice } from '@lib/wizard-session'; @@ -85,6 +85,9 @@ export interface AuthErrorDetail { } export interface WizardUI { + /** True when a person can answer prompts through this UI. */ + readonly interactive: boolean; + // ── Lifecycle messages ──────────────────────────────────────────── intro(message: string): void; /** Success outro with a plain text message. */ diff --git a/src/utils/wizard-abort.ts b/src/utils/wizard-abort.ts index 7805edcc4..31d4d13f9 100644 --- a/src/utils/wizard-abort.ts +++ b/src/utils/wizard-abort.ts @@ -9,7 +9,6 @@ import { analytics } from './analytics'; import { logToFile } from './debug'; import { getUI } from '@ui'; -import { LoggingUI } from '@ui/logging-ui'; import { OutroKind, type OutroData } from '@lib/wizard-session'; import type { ErrorCode } from '@lib/errors'; import { emitWizardError, sanitizeErrorDetail } from '@lib/errors'; @@ -124,8 +123,8 @@ export async function wizardAbort( await ui.waitForOutroDismissed(); // 6. Emit the machine-readable error line for non-interactive hosts - // (LoggingUI and its HeadlessUI subclass); the TUI never sees it. - if (code && ui instanceof LoggingUI) { + // (console renderers); the TUI never sees it. + if (code && !ui.interactive) { emitWizardError({ code, message: resolvedOutroData.message ?? message,