From 71e2a40357fa02720078c3be78dcd400ad662480 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 18 Sep 2026 19:19:57 -0400 Subject: [PATCH] chore(agent): give the agent a run contract instead of the program config ProgramRunConfig carries the fields the runner reads plus postAuthGateIds and healthCheckDeclared, computed by runConfigFor from the steps. Run steps name the program they run as data; the runners execute them. ProgramRun, AbortCase, PromptContext, the agent signal vocabulary, shouldDisableAsk, claude settings, env isolation, and token pricing move out of src/lib/agent. WizardUI gains `interactive` so wizard-abort no longer checks the renderer class. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- bin.ts | 2 +- scripts/tui-host.no-jest.ts | 14 +- .../architecture/known-violations.json | 36 +-- ...credential-isolation-snapshot.test.ts.snap | 0 .../__tests__/agent-env-isolation.test.ts | 2 +- src/lib/__tests__/agent-interface.test.ts | 2 +- .../__tests__/claude-settings-backup.test.ts | 2 +- .../credential-isolation-snapshot.test.ts | 6 +- .../__tests__/settings-conflicts.test.ts | 4 +- .../__tests__/token-pricing.test.ts | 2 +- src/lib/{agent => }/agent-env-isolation.ts | 0 .../{agent/signals.ts => agent-signals.ts} | 0 .../agent/__tests__/output-signals.test.ts | 2 +- src/lib/agent/agent-interface.ts | 10 +- src/lib/agent/agent-prompt.ts | 32 +-- src/lib/agent/agent-runner.ts | 1 + src/lib/agent/mcp-prompt-streaming.ts | 2 +- src/lib/agent/output-signals.ts | 2 +- .../harness/pi/__tests__/completion.test.ts | 2 +- .../harness/pi/__tests__/status-line.test.ts | 2 +- src/lib/agent/runner/harness/pi/completion.ts | 2 +- src/lib/agent/runner/harness/pi/index.ts | 2 +- src/lib/agent/runner/harness/pi/task.ts | 2 +- src/lib/agent/runner/harness/types.ts | 6 +- src/lib/agent/runner/index.ts | 13 +- src/lib/agent/runner/sequence/linear.ts | 6 +- .../orchestrator/orchestrator-runner.ts | 4 +- src/lib/agent/runner/shared/bootstrap.ts | 51 +--- src/lib/agent/runner/shared/types.ts | 121 +-------- src/lib/agent/runner/switchboard/sequence.ts | 4 +- src/lib/ask-policy.ts | 23 ++ src/lib/{agent => }/claude-settings.ts | 0 src/lib/errors/agent-map.ts | 2 +- src/lib/middleware/benchmarks/cost-tracker.ts | 2 +- src/lib/program-run.ts | 250 ++++++++++++++++++ .../post-auth-gates.test.ts.snap | 25 ++ .../programs/__tests__/agent-skill.test.ts | 2 +- .../programs/__tests__/error-tracking.test.ts | 2 +- .../__tests__/metrics-program.test.ts | 2 +- .../__tests__/post-auth-gates.test.ts | 26 +- .../__tests__/self-driving-prompt.test.ts | 2 +- src/lib/programs/agent-skill/index.ts | 2 +- src/lib/programs/audit/detect.ts | 2 +- src/lib/programs/audit/index.ts | 2 +- .../detect.ts | 2 +- .../index.ts | 2 +- .../prompt.ts | 2 +- src/lib/programs/error-tracking/index.ts | 2 +- src/lib/programs/events-audit/index.ts | 2 +- src/lib/programs/mcp-analytics/index.ts | 2 +- src/lib/programs/migration/index.ts | 2 +- src/lib/programs/posthog-integration/index.ts | 9 +- src/lib/programs/program-step.ts | 101 +------ src/lib/programs/replay-vision/index.ts | 2 +- src/lib/programs/revenue-analytics/detect.ts | 2 +- src/lib/programs/run-config.ts | 24 ++ src/lib/programs/self-driving/detect.ts | 2 +- src/lib/programs/self-driving/index.ts | 2 +- src/lib/programs/self-driving/prompt.ts | 4 +- src/lib/programs/warehouse-source/detect.ts | 2 +- src/lib/programs/warehouse-source/index.ts | 2 +- .../programs/web-analytics-doctor/detect.ts | 2 +- src/lib/runners/run-non-interactive.ts | 3 +- src/lib/runners/run-wizard.ts | 14 +- src/lib/{agent => }/token-pricing.ts | 0 src/lib/wizard-session.ts | 2 +- src/ui/logging-ui.ts | 3 +- src/ui/tui/__tests__/store-invariants.test.ts | 2 +- src/ui/tui/components/TokenCostHud.tsx | 2 +- src/ui/tui/exit-line.ts | 2 +- src/ui/tui/ink-ui.ts | 3 +- src/ui/tui/screens/ManagedSettingsScreen.tsx | 2 +- src/ui/tui/store.ts | 4 +- src/ui/wizard-ui.ts | 5 +- src/utils/wizard-abort.ts | 5 +- 75 files changed, 478 insertions(+), 411 deletions(-) rename src/lib/{agent => }/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap (100%) rename src/lib/{agent => }/__tests__/agent-env-isolation.test.ts (99%) rename src/lib/{agent => }/__tests__/claude-settings-backup.test.ts (99%) rename src/lib/{agent => }/__tests__/credential-isolation-snapshot.test.ts (96%) rename src/lib/{agent => }/__tests__/settings-conflicts.test.ts (98%) rename src/lib/{agent => }/__tests__/token-pricing.test.ts (99%) rename src/lib/{agent => }/agent-env-isolation.ts (100%) rename src/lib/{agent/signals.ts => agent-signals.ts} (100%) create mode 100644 src/lib/ask-policy.ts rename src/lib/{agent => }/claude-settings.ts (100%) create mode 100644 src/lib/program-run.ts create mode 100644 src/lib/programs/run-config.ts rename src/lib/{agent => }/token-pricing.ts (100%) 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,