From ebee145b3cd5a8f0e2677984e4551fa191a79951 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 18:08:54 -0400 Subject: [PATCH 01/61] refactor(agent): make runAgent a function with progress and interaction contracts runAgent(config, input, {onProgress?, interaction?, signal?}) returns a RunResult and never rejects. RunConfig and RunInput replace the session reads, AgentProgress replaces the getUI() calls, an optional AgentInteraction replaces the getUI() answerer, and every former wizardAbort returns as a failure with the same fields. An unexpected throw returns as outcome 'crashed' with the original error attached. Gates, authenticate, token refresh, flag fetch, binding resolution and the exit move to src/lib/programs/run-agent-legacy.ts, which maps each progress event to one WizardUI call so every existing caller keeps its output. authenticate moves to programs unchanged. The allowlist architecture test is removed; import enforcement lands with the A2 move. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- package.json | 1 - scripts/tui-host.no-jest.ts | 4 +- .../architecture/import-boundaries.test.ts | 403 ------------- .../architecture/known-violations.json | 76 --- src/__tests__/cli.test.ts | 2 +- src/__tests__/provision-cli.test.ts | 2 +- src/lib/__tests__/agent-interface.test.ts | 33 +- .../__tests__/run-agent-standalone.test.ts | 388 +++++++++++++ src/lib/agent/agent-interface.ts | 111 +++- src/lib/agent/agent-runner.ts | 3 +- src/lib/agent/progress.ts | 100 ++++ src/lib/agent/runner/README.md | 58 +- .../agent/runner/harness/anthropic/index.ts | 46 +- src/lib/agent/runner/harness/pi/index.ts | 74 ++- src/lib/agent/runner/harness/pi/task.ts | 18 +- src/lib/agent/runner/harness/pi/tasks.ts | 33 +- src/lib/agent/runner/harness/types.ts | 39 +- src/lib/agent/runner/index.ts | 285 ++++------ src/lib/agent/runner/sequence/linear.ts | 279 ++++----- .../__tests__/seeded-decline-skip.test.ts | 3 - .../__tests__/task-notice-timeout.test.ts | 43 +- .../runner/sequence/orchestrator/executor.ts | 15 + .../orchestrator/orchestrator-runner.ts | 226 +++++--- src/lib/agent/runner/shared/ask.ts | 42 ++ src/lib/agent/runner/shared/bootstrap.ts | 327 ++--------- src/lib/agent/runner/shared/errors.ts | 16 +- .../agent/runner/shared/progress-collector.ts | 117 ++++ src/lib/agent/runner/shared/types.ts | 238 +++++++- src/lib/agent/runner/switchboard/sequence.ts | 20 +- .../detection/__tests__/project-scope.test.ts | 4 +- src/lib/detection/project-scope.ts | 2 +- .../refresh-access-token-if-needed.test.ts | 0 .../shared => programs}/authenticate.ts | 2 +- src/lib/programs/posthog-integration/index.ts | 5 +- src/lib/programs/run-agent-legacy.ts | 530 ++++++++++++++++++ .../runners/__tests__/mint-recovery.test.ts | 4 +- src/lib/runners/run-non-interactive.ts | 2 +- src/lib/runners/run-wizard.ts | 4 +- vitest.config.ts | 4 +- 39 files changed, 2180 insertions(+), 1379 deletions(-) delete mode 100644 src/__tests__/architecture/import-boundaries.test.ts delete mode 100644 src/__tests__/architecture/known-violations.json create mode 100644 src/lib/agent/__tests__/run-agent-standalone.test.ts create mode 100644 src/lib/agent/progress.ts create mode 100644 src/lib/agent/runner/shared/ask.ts create mode 100644 src/lib/agent/runner/shared/progress-collector.ts rename src/lib/{agent/runner/shared => programs}/__tests__/refresh-access-token-if-needed.test.ts (100%) rename src/lib/{agent/runner/shared => programs}/authenticate.ts (98%) create mode 100644 src/lib/programs/run-agent-legacy.ts diff --git a/package.json b/package.json index 4df515d69..4a2b82474 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,6 @@ "test:tui": "vitest run --project tui", "test:cli": "vitest run --project cli", "test:harness": "vitest run --project harness", - "test:arch": "vitest run --project architecture", "test:goldens:update": "vitest run -u", "test:coverage": "pnpm build && vitest run --coverage", "test:e2e": "pnpm build && ./e2e-tests/run.sh", diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index ded2d55a2..ead31e380 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -27,10 +27,10 @@ import type { Harness, Sequence } from '@lib/constants'; 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 { runAgent } from '@lib/programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; import { getAuditChecks } from '@lib/programs/audit/types'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { logToFile } from '@utils/debug'; import { join } from 'path'; diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts deleted file mode 100644 index 8220182ae..000000000 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ /dev/null @@ -1,403 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { fileURLToPath } from 'url'; - -export type Surface = 'env' | 'legacy' | 'agent' | 'tui' | 'cli'; - -const HERE = path.dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = path.resolve(HERE, '../../..'); - -const SURFACE_RULES: ReadonlyArray boolean]> = - [ - ['env', (p) => p === 'src/env.ts'], - [ - 'agent', - (p) => - p.startsWith('src/lib/agent/') || - p.startsWith('src/lib/middleware/') || - p === 'src/lib/gateway-session.ts' || - p === 'src/lib/yara-hooks.ts' || - p === 'src/lib/yara-policy.ts' || - p === 'src/lib/wizard-tools/mcp.ts', - ], - [ - 'tui', - (p) => - p.startsWith('src/ui/tui/') || - p === 'src/ui/logging-ui.ts' || - p === 'src/ui/headless-ui.ts' || - p === 'src/commands/factories/family-picker.tsx' || - /^src\/lib\/programs\/[^/]+\/content\//.test(p) || - /^src\/lib\/programs\/[^/]+\/tips\.ts$/.test(p), - ], - [ - 'cli', - (p) => - p === 'bin.ts' || - p === 'src/wizard.ts' || - p === 'src/telemetry.ts' || - p.startsWith('src/commands/') || - p.startsWith('src/lib/runners/'), - ], - ]; - -export function classifySurface(relPath: string): Surface { - const p = relPath.split(path.sep).join('/'); - for (const [surface, matches] of SURFACE_RULES) { - if (matches(p)) return surface; - } - return 'legacy'; -} - -export const ALLOWED_IMPORTS: Record = { - env: [], - legacy: ['env', 'legacy'], - agent: ['env', 'legacy', 'agent'], - tui: ['env', 'legacy', 'tui'], - cli: ['env', 'legacy', 'agent', 'tui', 'cli'], -}; - -const TUI_ONLY_PACKAGES = ['ink', 'react', '@inkjs/ui', 'ink-testing-library']; - -const SKIP_DIRS = new Set([ - '__tests__', - '__mocks__', - '__fixtures__', - '__snapshots__', - 'node_modules', - 'dist', - 'coverage', -]); - -const SPECIFIER_PATTERNS = [ - /import\s+(?:type\s+)?[^'"]*?from\s*['"]([^'"]+)['"]/g, - /import\s*['"]([^'"]+)['"]/g, - /export\s+(?:type\s+)?[^'"]*?from\s*['"]([^'"]+)['"]/g, - /import\(\s*['"]([^'"]+)['"]\s*\)/g, - /require\(\s*['"]([^'"]+)['"]\s*\)/g, -]; - -const REGEX_PRECEDERS = new Set([ - '\n', - '(', - ')', - ',', - '=', - ':', - '[', - '!', - '&', - '|', - '?', - '{', - '}', - ';', - '+', - '-', - '*', - '%', - '^', - '~', - '<', - '>', -]); - -function endOfString(source: string, start: number, quote: string): number { - let i = start + 1; - while (i < source.length) { - if (source[i] === '\\') { - i += 2; - continue; - } - if (source[i] === quote) return i + 1; - i++; - } - return source.length; -} - -function endOfRegex(source: string, start: number): number { - let i = start + 1; - let inClass = false; - while (i < source.length) { - const c = source[i]; - if (c === '\\') { - i += 2; - continue; - } - if (c === '\n') return start + 1; - if (c === '[') inClass = true; - else if (c === ']') inClass = false; - else if (c === '/' && !inClass) return i + 1; - i++; - } - return source.length; -} - -function stripComments(source: string): string { - let out = ''; - let prev = '\n'; - let i = 0; - while (i < source.length) { - const c = source[i]; - const next = source[i + 1]; - if (c === '/' && next === '/') { - while (i < source.length && source[i] !== '\n') i++; - continue; - } - if (c === '/' && next === '*') { - i += 2; - while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) - i++; - i += 2; - out += ' '; - continue; - } - if (c === '"' || c === "'" || c === '`') { - const end = endOfString(source, i, c); - out += source.slice(i, end); - prev = c; - i = end; - continue; - } - if (c === '/' && REGEX_PRECEDERS.has(prev)) { - const end = endOfRegex(source, i); - out += source.slice(i, end); - prev = '/'; - i = end; - continue; - } - out += c; - if (c === '\n' || !/\s/.test(c)) prev = c; - i++; - } - return out; -} - -function toRepoRelative(abs: string): string { - return path.relative(REPO_ROOT, abs).split(path.sep).join('/'); -} - -function isFile(abs: string): boolean { - return fs.statSync(abs, { throwIfNoEntry: false })?.isFile() ?? false; -} - -function collectFiles(absDir: string, into: string[]): void { - for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) { - const abs = path.join(absDir, entry.name); - if (entry.isDirectory()) { - if (!SKIP_DIRS.has(entry.name)) collectFiles(abs, into); - continue; - } - if (!entry.isFile()) continue; - if (!/\.tsx?$/.test(entry.name)) continue; - if (/\.d\.ts$/.test(entry.name) || /\.test\.tsx?$/.test(entry.name)) - continue; - into.push(toRepoRelative(abs)); - } -} - -function loadAliases(): ReadonlyArray { - const tsconfig = JSON.parse( - fs.readFileSync(path.join(REPO_ROOT, 'tsconfig.build.json'), 'utf8'), - ) as { compilerOptions?: { paths?: Record } }; - return Object.entries(tsconfig.compilerOptions?.paths ?? {}).map( - ([pattern, targets]) => [pattern, targets[0]] as const, - ); -} - -function aliasTarget( - spec: string, - aliases: ReadonlyArray, -): string | null { - for (const [pattern, target] of aliases) { - if (pattern.endsWith('*')) { - const prefix = pattern.slice(0, -1); - if (spec.startsWith(prefix)) { - return path.resolve( - REPO_ROOT, - target.slice(0, -1) + spec.slice(prefix.length), - ); - } - } else if (spec === pattern) { - return path.resolve(REPO_ROOT, target); - } - } - return null; -} - -function probe(base: string): string | null { - const candidates: string[] = []; - if (base.endsWith('.js')) { - const stem = base.slice(0, -3); - candidates.push(`${stem}.ts`, `${stem}.tsx`); - } - candidates.push( - `${base}.ts`, - `${base}.tsx`, - path.join(base, 'index.ts'), - path.join(base, 'index.tsx'), - base, - ); - return candidates.find(isFile) ?? null; -} - -function specifiersIn(text: string): string[] { - const found = new Set(); - for (const pattern of SPECIFIER_PATTERNS) { - pattern.lastIndex = 0; - let match = pattern.exec(text); - while (match !== null) { - found.add(match[1]); - match = pattern.exec(text); - } - } - return [...found]; -} - -type Analysis = { - files: string[]; - edges: string[]; - violations: ReadonlyArray<{ key: string; rule: string }>; - unresolved: string[]; -}; - -function analyze(): Analysis { - const files: string[] = ['bin.ts']; - collectFiles(path.join(REPO_ROOT, 'src'), files); - files.sort(); - - const aliases = loadAliases(); - const edges = new Set(); - const violations = new Map(); - const unresolved = new Set(); - - for (const file of files) { - const text = stripComments( - fs.readFileSync(path.join(REPO_ROOT, file), 'utf8'), - ); - const from = classifySurface(file); - const allowed = ALLOWED_IMPORTS[from]; - - for (const spec of specifiersIn(text)) { - const base = spec.startsWith('.') - ? path.resolve(REPO_ROOT, path.dirname(file), spec) - : aliasTarget(spec, aliases); - - if (base === null) { - const tuiOnly = - TUI_ONLY_PACKAGES.includes(spec) || spec.startsWith('react/'); - if (tuiOnly && from !== 'tui') { - violations.set(`${file} -> pkg:${spec}`, 'ink-outside-tui'); - } - continue; - } - - const resolved = probe(base); - if (resolved === null) { - unresolved.add(`${file} -> ${spec}`); - continue; - } - - const target = toRepoRelative(resolved); - const key = `${file} -> ${target}`; - edges.add(key); - - const to = classifySurface(target); - if (!allowed.includes(to)) violations.set(key, `matrix:${from}->${to}`); - } - } - - return { - files, - edges: [...edges].sort(), - violations: [...violations] - .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([key, rule]) => ({ key, rule })), - unresolved: [...unresolved].sort(), - }; -} - -const analysis = analyze(); - -const known = ( - JSON.parse( - fs.readFileSync(path.join(HERE, 'known-violations.json'), 'utf8'), - ) as { violations: string[] } -).violations; - -if (process.env.PRINT_VIOLATIONS) { - const byRule = new Map(); - const byImporter = new Map(); - for (const { key, rule } of analysis.violations) { - byRule.set(rule, (byRule.get(rule) ?? 0) + 1); - const file = key.split(' -> ')[0]; - byImporter.set(file, (byImporter.get(file) ?? 0) + 1); - } - const lines = [ - `files scanned: ${analysis.files.length}`, - `edges: ${analysis.edges.length}`, - `violations: ${analysis.violations.length}`, - `unresolved: ${analysis.unresolved.length}`, - ...analysis.unresolved.map((u) => ` unresolved: ${u}`), - ...[...byRule] - .sort((a, b) => b[1] - a[1]) - .map(([rule, count]) => ` ${rule}: ${count}`), - 'top importers:', - ...[...byImporter] - .sort((a, b) => b[1] - a[1]) - .slice(0, 10) - .map(([file, count]) => ` ${file}: ${count}`), - ]; - process.stderr.write(`${lines.join('\n')}\n`); - process.stderr.write( - `${JSON.stringify( - { violations: analysis.violations.map((v) => v.key) }, - null, - 2, - )}\n`, - ); -} - -describe('import boundaries', () => { - it('resolves every internal specifier', () => { - expect(analysis.unresolved).toEqual([]); - }); - - it('introduces no violation outside known-violations.json', () => { - const knownSet = new Set(known); - const added = analysis.violations - .filter(({ key }) => !knownSet.has(key)) - .map(({ key, rule }) => `${key} [${rule}]`); - expect(added).toEqual([]); - }); - - it('keeps known-violations.json free of stale entries', () => { - const current = new Set(analysis.violations.map((v) => v.key)); - const stale = known.filter((key) => !current.has(key)); - expect( - stale, - 'stale entries, delete them from known-violations.json', - ).toEqual([]); - }); -}); - -describe('surface classification', () => { - it('maps representative paths to their surface', () => { - expect(classifySurface('src/env.ts')).toBe('env'); - expect(classifySurface('src/utils/analytics.ts')).toBe('legacy'); - expect(classifySurface('src/lib/agent/agent-runner.ts')).toBe('agent'); - expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); - expect(classifySurface('bin.ts')).toBe('cli'); - expect(classifySurface('src/lib/wizard-tools/mcp.ts')).toBe('agent'); - expect(classifySurface('src/lib/wizard-tools/tools.ts')).toBe('legacy'); - expect(classifySurface('src/commands/factories/family-picker.tsx')).toBe( - 'tui', - ); - expect( - classifySurface('src/lib/programs/posthog-integration/content/index.tsx'), - ).toBe('tui'); - expect( - classifySurface('src/lib/programs/posthog-integration/index.ts'), - ).toBe('legacy'); - }); -}); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json deleted file mode 100644 index d523b2a73..000000000 --- a/src/__tests__/architecture/known-violations.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "violations": [ - "src/commands/factories/family-picker.tsx -> src/commands/command.ts", - "src/env.ts -> src/lib/headless-mode.ts", - "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", - "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", - "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" - ] -} diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 2b8a723f7..e4ebbe951 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -112,7 +112,7 @@ vi.mock('../utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../lib/agent/agent-runner', () => ({ +vi.mock('../lib/programs/run-agent-legacy', () => ({ runAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index d1b561ba4..10b6539b2 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -63,7 +63,7 @@ vi.mock('../utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../lib/agent/agent-runner', () => ({ +vi.mock('../lib/programs/run-agent-legacy', () => ({ runAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/lib/__tests__/agent-interface.test.ts b/src/lib/__tests__/agent-interface.test.ts index 3c48975be..7ca36c2ca 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/lib/__tests__/agent-interface.test.ts @@ -12,7 +12,6 @@ import { import { AgentOutputSignals } from '@lib/agent/output-signals'; import { RESUME_INSTRUCTION } from '@lib/agent/signals'; import { analytics } from '@utils/analytics'; -import { wizardAbort } from '@utils/wizard-abort'; import { Sequence } from '@lib/constants'; import type { WizardRunOptions } from '@utils/types'; import type { SpinnerHandle } from '@ui'; @@ -24,11 +23,6 @@ import { // Mock dependencies vi.mock('../../utils/analytics'); vi.mock('../../utils/debug'); -// wizardAbort exits the process; the 401 tests below need it to just reject. -vi.mock('@utils/wizard-abort', async (importOriginal) => ({ - ...(await importOriginal()), - wizardAbort: vi.fn(), -})); // Mock the SDK module const mockQuery = vi.fn(); @@ -695,6 +689,10 @@ describe('subprocess gateway credentials', () => { describe('gateway re-mint on 401', () => { const spinner = { start: vi.fn(), stop: vi.fn(), message: vi.fn() }; + // Where the run reports the auth screen; stands where getUI() used to. + const emit = vi.fn(); + const authErrors = () => + emit.mock.calls.filter(([e]) => e.kind === 'authError'); const options: WizardRunOptions = { debug: false, installDir: '/test/dir', @@ -722,6 +720,7 @@ describe('gateway re-mint on 401', () => { triageProvider: () => Promise.resolve('false_positive'), gatewayAuth, refreshGatewayAuth, + emit, }); const run = (cfg: ReturnType) => runAgent(cfg, 'test prompt', options, spinner as unknown as SpinnerHandle, { @@ -778,7 +777,7 @@ describe('gateway re-mint on 401', () => { beforeEach(() => { vi.clearAllMocks(); mockUIInstance.spinner.mockReturnValue(spinner); - vi.mocked(wizardAbort).mockRejectedValue(new Error('wizardAbort: exit')); + emit.mockReset(); }); it('mints once and resumes the session when an aged bearer is rejected', async () => { @@ -794,7 +793,7 @@ describe('gateway re-mint on 401', () => { expect(result).toEqual({}); expect(refresh).toHaveBeenCalledTimes(1); - expect(wizardAbort).not.toHaveBeenCalled(); + expect(authErrors()).toHaveLength(0); expect(mockQuery).toHaveBeenCalledTimes(2); const [first, second] = mockQuery.mock.calls.map((c) => c[0]); expect(first.options.resume).toBeUndefined(); @@ -833,11 +832,12 @@ describe('gateway re-mint on 401', () => { expect(refresh).toHaveBeenCalledTimes(1); expect(mockQuery).toHaveBeenCalledTimes(2); - expect(mockUIInstance.showAuthError).toHaveBeenCalledTimes(1); - expect(wizardAbort).toHaveBeenCalledTimes(1); - // In production wizardAbort exits; the mocked rejection surfaces as the - // run's API error. - expect(result.error).toBe('WIZARD_API_ERROR'); + expect(authErrors()).toHaveLength(1); + // The auth screen is reported, and the decided failure goes back to the + // caller, which owns the exit. + expect(result.error).toBeUndefined(); + expect(result.failure?.message).toBe('Authentication failed (401)'); + expect(result.failure?.code).toBeDefined(); }); it('judges a failed resumed session on its own error, not the old 401', async () => { @@ -874,7 +874,7 @@ describe('gateway re-mint on 401', () => { expect(result.error).toBe('WIZARD_API_ERROR'); expect(result.message).toContain('500'); expect(result.message).not.toContain('401'); - expect(mockUIInstance.showAuthError).not.toHaveBeenCalled(); + expect(authErrors()).toHaveLength(0); }); it('does not re-mint when a fresh bearer is rejected', async () => { @@ -888,9 +888,8 @@ describe('gateway re-mint on 401', () => { // A fresh token the gateway rejects is a bad credential, not age. expect(refresh).not.toHaveBeenCalled(); expect(mockQuery).toHaveBeenCalledTimes(1); - expect(mockUIInstance.showAuthError).toHaveBeenCalledTimes(1); - expect(wizardAbort).toHaveBeenCalledTimes(1); - expect(result.error).toBe('WIZARD_API_ERROR'); + expect(authErrors()).toHaveLength(1); + expect(result.failure?.message).toBe('Authentication failed (401)'); }); }); diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts new file mode 100644 index 000000000..9a08c7569 --- /dev/null +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -0,0 +1,388 @@ +/** + * `runAgent(config, input, options)` runs with no UI, no store, no session and + * no program registry: a fake harness stands in for the SDK, and everything + * the run reports arrives through `onProgress` or comes back in the result. + * + * The `@ui` mock below throws on use. It is never reached — that is the + * assertion the whole file rests on. + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Harness, Sequence } from '@lib/constants'; +import { HostResolution } from '@lib/host-resolution'; +import { OutroKind, type PendingQuestion } from '@lib/wizard-session'; +import { AGENT_ERROR_CODE, ErrorCodes } from '@lib/errors'; +import { AgentErrorType } from '@lib/agent/signals'; +import type { AgentProgress } from '@lib/agent/progress'; +import type { + AgentHarness, + BackendRunInputs, +} from '@lib/agent/runner/harness/types'; + +vi.mock('@ui', () => ({ + getUI: () => { + throw new Error('the agent reached for getUI()'); + }, + setUI: () => { + throw new Error('the agent reached for setUI()'); + }, +})); +vi.mock('@utils/debug'); +vi.mock('@utils/analytics', () => ({ + analytics: { + build: 'test', + runId: 'run-1', + wizardCapture: vi.fn(), + capture: vi.fn(), + captureException: vi.fn(), + setTag: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }, +})); +vi.mock('@lib/gateway-session', async (importOriginal) => ({ + ...(await importOriginal()), + gatewayAuth: vi.fn().mockResolvedValue({ + gatewayUrl: 'https://gateway.test', + token: 'phe_run', + teamId: 1, + refreshAtMs: Date.now() + 3_600_000, + }), +})); + +// The fake harness: reports a little of everything, then returns what the +// current test told it to. +const harnessState = vi.hoisted(() => ({ + result: {} as { error?: string; message?: string; failure?: unknown }, + throws: undefined as Error | undefined, + lastInputs: undefined as unknown, + askQuestions: undefined as PendingQuestion['questions'] | undefined, +})); +vi.mock('@lib/agent/runner/switchboard/harness', () => { + const fake: AgentHarness = { + name: Harness.pi, + async run(inputs: BackendRunInputs) { + harnessState.lastInputs = inputs; + const { emit, spinner } = inputs; + emit({ kind: 'log', level: 'step', message: 'Initializing agent' }); + spinner.start('Working'); + emit({ kind: 'status', message: 'Installing the SDK' }); + emit({ + kind: 'tasks', + tasks: [{ content: 'Install', status: 'completed' }], + }); + emit({ kind: 'url', which: 'dashboard', url: 'https://d/1' }); + emit({ + kind: 'usage', + delta: { + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: 0, + cacheCreationTokens: 0, + cacheCreation5m: 0, + cacheCreation1h: 0, + }, + }); + if (harnessState.askQuestions && inputs.askBridge) { + const { answers } = await inputs.askBridge.request({ + questions: harnessState.askQuestions, + }); + emit({ + kind: 'status', + message: `answered:${JSON.stringify(answers)}`, + }); + } + if (harnessState.throws) throw harnessState.throws; + spinner.stop('Done'); + return harnessState.result as never; + }, + }; + return { + HARNESS_OPTIONS: { [Harness.pi]: fake }, + getHarness: () => fake, + resolveHarness: () => ({ harness: Harness.pi, model: 'm' }), + }; +}); + +import { runAgent } from '@lib/agent/runner'; +import type { RunConfig, RunInput } from '@lib/agent/runner'; +import { analytics } from '@utils/analytics'; + +let tmp: string; + +const config = (over: Partial = {}): RunConfig => ({ + programId: 'test-program', + run: { + integrationLabel: 'test-integration', + spinnerMessage: 'Working...', + successMessage: 'Done!', + estimatedDurationMinutes: 1, + reportFile: 'report.md', + docsUrl: 'https://docs.test', + abortCases: [ + { + match: /no stripe/i, + message: 'No Stripe here', + body: 'Stripe is required.', + errorCode: ErrorCodes.AgentAbort, + }, + ], + }, + composed: false, + binding: { sequence: Sequence.linear, harness: Harness.pi, model: 'm' }, + switchboard: { program: 'test-program', flags: {} }, + skillsBaseUrl: 'https://skills.test', + wizardFlags: {}, + wizardFlagPayloads: {}, + wizardMetadata: {}, + ...over, +}); + +const input = (over: Partial = {}): RunInput => ({ + installDir: tmp, + credentials: { + accessToken: 'tok', + projectApiKey: 'phc_test', + host: HostResolution.fromApiHost('https://us.posthog.com'), + projectId: 1, + }, + project: null, + apiUser: null, + skillId: 'test-integration', + flags: { + ci: false, + signup: false, + debug: false, + e2eAsk: false, + localMcp: false, + captureAio: false, + benchmark: false, + yaraReport: false, + }, + host: {}, + ...over, +}); + +beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'run-agent-standalone-')); + harnessState.result = {}; + harnessState.throws = undefined; + harnessState.lastInputs = undefined; + harnessState.askQuestions = undefined; + vi.mocked(analytics.shutdown).mockClear(); +}); + +afterEach(() => fs.rmSync(tmp, { recursive: true, force: true })); + +describe('runAgent standalone', () => { + it('runs to success with an observer and an answerer', async () => { + const events: AgentProgress[] = []; + const ask = vi.fn(() => Promise.resolve({ q1: 'yes' })); + harnessState.askQuestions = [ + { id: 'q1', prompt: 'Continue?', kind: 'single' as const }, + ]; + + const result = await runAgent(config(), input(), { + onProgress: (e) => events.push(e), + interaction: { ask }, + }); + + expect(result.outcome).toBe('success'); + expect(result.skillId).toBe('test-integration'); + expect(result.outro).toEqual({ + kind: OutroKind.Success, + message: 'Done!', + reportFile: 'report.md', + docsUrl: 'https://docs.test', + continueUrl: undefined, + }); + expect(result.failure).toBeUndefined(); + + // The answerer saw the question the bridge built, and its answer came back. + expect(ask).toHaveBeenCalledTimes(1); + const [question] = ask.mock.calls[0] as unknown as [PendingQuestion]; + expect(question.questions[0].id).toBe('q1'); + expect(question.source).toBe('test-integration'); + expect(events).toContainEqual({ + kind: 'status', + message: 'answered:{"q1":"yes"}', + }); + + // Emission order: started first, completion then completed last. + const kinds = events.map( + (e) => `${e.kind}${'phase' in e ? `:${e.phase}` : ''}`, + ); + expect(kinds[0]).toBe('lifecycle:started'); + expect(kinds.slice(-2)).toEqual(['completion', 'lifecycle:completed']); + + // The agent's own snapshot, independent of the observer. + expect(result.snapshot.dashboardUrl).toBe('https://d/1'); + expect(result.snapshot.tasks).toEqual([ + { content: 'Install', status: 'completed' }, + ]); + expect(result.snapshot.statusMessages[0]).toBe('Installing the SDK'); + expect(result.snapshot.usage).toEqual({ + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: 0, + cacheCreationTokens: 0, + }); + expect(analytics.shutdown).toHaveBeenCalledWith('success'); + }); + + it('runs to a complete result with no options at all', async () => { + const result = await runAgent(config(), input()); + + expect(result.outcome).toBe('success'); + expect(result.outro?.kind).toBe(OutroKind.Success); + expect(result.snapshot.dashboardUrl).toBe('https://d/1'); + // No answerer: no bridge is installed, so `wizard_ask` reports unavailable + // rather than hanging. + const inputs = harnessState.lastInputs as BackendRunInputs; + expect(inputs.askBridge).toBeUndefined(); + }); + + it('installs no bridge in CI even with an answerer, as before', async () => { + await runAgent( + config(), + input({ + flags: { + ci: true, + signup: false, + debug: false, + e2eAsk: false, + localMcp: false, + captureAio: false, + benchmark: false, + yaraReport: false, + }, + }), + { interaction: { ask: () => Promise.resolve({}) } }, + ); + const inputs = harnessState.lastInputs as BackendRunInputs; + expect(inputs.askBridge).toBeUndefined(); + }); + + it('returns an agent abort as a decided failure with the matched case', async () => { + harnessState.result = { + error: AgentErrorType.ABORT, + message: 'No Stripe found', + }; + const events: AgentProgress[] = []; + + const result = await runAgent(config(), input(), { + onProgress: (e) => events.push(e), + }); + + expect(result.outcome).toBe('aborted'); + expect(result.failure?.code).toBe(ErrorCodes.AgentAbort); + expect(result.failure?.outroData).toMatchObject({ + kind: OutroKind.Error, + message: 'No Stripe here', + body: 'Stripe is required.', + errorDetail: { reason: 'No Stripe found' }, + }); + expect(result.failure?.error?.message).toBe( + 'Agent aborted: No Stripe found', + ); + expect(events.some((e) => e.kind === 'completion')).toBe(false); + expect(analytics.shutdown).not.toHaveBeenCalled(); + }); + + it('returns a coded failure for a harness error', async () => { + harnessState.result = { error: AgentErrorType.NO_PROGRESS }; + + const result = await runAgent(config(), input()); + + expect(result.outcome).toBe('failed'); + expect(result.failure?.code).toBe( + AGENT_ERROR_CODE[AgentErrorType.NO_PROGRESS], + ); + expect(result.failure?.message).toContain('without changing your project'); + }); + + it('passes a harness-decided failure through untouched', async () => { + const failure = { code: ErrorCodes.AgentAbort, message: 'decided' }; + harnessState.result = { failure }; + + const result = await runAgent(config(), input()); + + expect(result.outcome).toBe('failed'); + expect(result.failure).toBe(failure); + }); + + it('skips the terminal outro and the shutdown for a composed sub-run', async () => { + const events: AgentProgress[] = []; + + const result = await runAgent(config({ composed: true }), input(), { + onProgress: (e) => events.push(e), + }); + + expect(result.outcome).toBe('success'); + expect(result.outro).toBeUndefined(); + expect(events.some((e) => e.kind === 'completion')).toBe(false); + expect( + events.some((e) => e.kind === 'lifecycle' && e.phase === 'completed'), + ).toBe(false); + expect(analytics.shutdown).not.toHaveBeenCalled(); + }); + + it('finishes when the observer throws on every event', async () => { + const result = await runAgent(config(), input(), { + onProgress: () => { + throw new Error('projection broke'); + }, + }); + + expect(result.outcome).toBe('success'); + expect(result.snapshot.tasks).toHaveLength(1); + }); + + it('calls the bound hooks with the run credentials', async () => { + const postRun = vi.fn().mockResolvedValue(undefined); + const buildOutroData = vi.fn(() => ({ + kind: OutroKind.Success, + message: 'custom', + })); + + const result = await runAgent( + config({ hooks: { postRun, buildOutroData } }), + input(), + ); + + expect(postRun).toHaveBeenCalledWith( + expect.objectContaining({ projectApiKey: 'phc_test' }), + ); + expect(buildOutroData).toHaveBeenCalledTimes(1); + expect(result.outro).toEqual({ + kind: OutroKind.Success, + message: 'custom', + }); + }); + + it('returns a crash as a result instead of rejecting', async () => { + const boom = new Error('SDK exploded'); + harnessState.throws = boom; + + const result = await runAgent(config(), input()); + + expect(result.outcome).toBe('crashed'); + expect(result.failure?.error).toBe(boom); + expect(result.failure?.message).toBe('SDK exploded'); + expect(result.failure?.code).toBe(ErrorCodes.InternalUnhandled); + // What was reported before the crash survives in the snapshot. + expect(result.snapshot.statusMessages).toContain('Installing the SDK'); + }); + + it('is cancelled by a signal that is already aborted', async () => { + const controller = new AbortController(); + controller.abort(); + + const result = await runAgent(config(), input(), { + signal: controller.signal, + }); + + expect(result.outcome).toBe('cancelled'); + expect(harnessState.lastInputs).toBeUndefined(); + }); +}); diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 815498a25..602923329 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -6,8 +6,11 @@ import path from 'path'; import * as os from 'os'; import { createRequire } from 'node:module'; -import { getUI, type SpinnerHandle } from '@ui'; -import type { TokenUsageDelta } from '@ui/wizard-ui'; +import type { + ProgressEmitter, + SpinnerHandle, + TokenUsageDelta, +} from './progress'; import { debug, logToFile, initLogFile, getLogFilePath } from '@utils/debug'; import type { WizardRunOptions } from '@utils/types'; import { analytics } from '@utils/analytics'; @@ -27,7 +30,8 @@ import { type AdditionalFeature, ADDITIONAL_FEATURE_PROMPTS, } from '@lib/wizard-session'; -import { wizardAbort, WizardError } from '@utils/wizard-abort'; +import { WizardError } from '@utils/wizard-abort'; +import type { AgentFailure } from './runner/shared/types'; import { createCustomHeaders } from '@utils/custom-headers'; import type { HostResolution } from '@lib/host-resolution'; import { @@ -243,6 +247,12 @@ export type AgentConfig = { * `--capture-aio` is off. Constructed once per run by the harness. */ capture?: AioCapture; + /** + * Where the run reports: log lines, tasks, status, URLs, usage, the handoff + * document and the auth-error detail. Absent → the run reports nowhere and + * still completes. + */ + emit?: ProgressEmitter; }; /** @@ -354,8 +364,12 @@ type AgentRunConfig = { program?: string; /** Resolved sequence, for the sequence-axis commandments. */ sequence: Sequence; + /** Where the run reports. A no-op when the caller passed none. */ + emit?: ProgressEmitter; }; +const NO_PROGRESS: ProgressEmitter = () => undefined; + /** * Global identifiers attached to every LLM gateway trace for a run. They ride on * each `$ai_generation` the gateway emits (in the `X-PostHog-Properties` blob @@ -531,6 +545,7 @@ export async function initializeAgent( initLogFile(); logToFile('Agent initialization starting'); logToFile('Install directory:', options.installDir); + const emit = config.emit ?? NO_PROGRESS; try { // Configure model routing (inherited by the SDK subprocess). All model @@ -661,6 +676,7 @@ export async function initializeAgent( program: config.integrationLabel, // A queue context is present only on a task run; that is the sequence. sequence: config.orchestrator ? Sequence.orchestrator : Sequence.linear, + emit, }; logToFile('Agent config:', { @@ -689,9 +705,11 @@ export async function initializeAgent( return agentRunConfig; } catch (error) { - getUI().log.error( - `Failed to initialize agent: ${(error as Error).message}`, - ); + emit({ + kind: 'log', + level: 'error', + message: `Failed to initialize agent: ${(error as Error).message}`, + }); logToFile('Agent initialization error:', error); debug('Agent initialization error:', error); throw error; @@ -740,7 +758,12 @@ export async function runAgent( onMessage(message: any): void; finalize(resultMessage: any, totalDurationMs: number): any; }, -): Promise<{ error?: AgentErrorType; message?: string }> { +): Promise<{ + error?: AgentErrorType; + message?: string; + failure?: AgentFailure; +}> { + const emit = agentConfig.emit ?? NO_PROGRESS; const { spinnerMessage = 'Customizing your PostHog setup...', successMessage = 'PostHog integration complete', @@ -854,7 +877,7 @@ export async function runAgent( // CostTrackerPlugin.onFinalize uses to correct per-turn drift. const totalCostUsd = Number(lastResultMessage?.total_cost_usd ?? 0); if (totalCostUsd > 0) { - getUI().setFinalTokenCostUsd(totalCostUsd); + emit({ kind: 'finalCost', usd: totalCostUsd }); } try { middleware?.finalize(lastResultMessage, durationMs); @@ -879,6 +902,9 @@ export async function runAgent( let sessionId: string | undefined; let reminted = false; let remintRequested = false; + // A 401 on a fresh bearer: the auth screen was reported, and this is the + // failure the caller ends the run with. The query is aborted to unwind. + let authFailure: AgentFailure | undefined; const agentConfigDir = createIsolatedAgentConfigDir(); try { @@ -1162,6 +1188,7 @@ export async function runAgent( options, spinner, signals, + emit, receivedSuccessResult, tasks, agentConfig.suppressTaskRender ?? false, @@ -1240,15 +1267,20 @@ export async function runAgent( ...authError, sessionExpired, }); - getUI().showAuthError({ - hasSettingsConflict: authError.hasSettingsConflict, - conflicts: authError.conflicts, - usingManagedLogin: authError.usingManagedLogin, - credentialPlaces: authError.credentialPlaces, - sessionExpired, - logFilePath: getLogFilePath(), + emit({ + kind: 'authError', + detail: { + hasSettingsConflict: authError.hasSettingsConflict, + conflicts: authError.conflicts, + usingManagedLogin: authError.usingManagedLogin, + credentialPlaces: authError.credentialPlaces, + sessionExpired, + logFilePath: getLogFilePath(), + }, }); - await wizardAbort({ + // The caller ends the run with this; the query is abandoned here + // where the process used to exit. + authFailure = { code: authCode, message: 'Authentication failed (401)', error: new WizardError( @@ -1264,7 +1296,9 @@ export async function runAgent( }, authCode, ), - }); + }; + abortController.abort(); + break; } try { @@ -1290,6 +1324,7 @@ export async function runAgent( } catch (error) { // The abort we asked for; anything else belongs to the outer catch. if (remintRequested) return 'remint'; + if (authFailure) return 'done'; throw error; } return remintRequested ? 'remint' : 'done'; @@ -1321,6 +1356,12 @@ export async function runAgent( await runQuery(sessionId); } + // A fresh bearer was rejected. The auth screen is already up; hand the + // decided failure to the caller, which owns the exit. + if (authFailure) { + return { failure: authFailure }; + } + // A YARA hook detected a terminal violation and aborted the run. if (yaraViolationReason) { logToFile('Agent error: YARA_VIOLATION'); @@ -1421,14 +1462,19 @@ export async function runAgent( // No API error found, re-throw the original exception spinner.stop(errorMessage); - getUI().log.error(`Error: ${(error as Error).message}`); + emit({ + kind: 'log', + level: 'error', + message: `Error: ${(error as Error).message}`, + }); logToFile('Agent run failed:', error); debug('Full error:', error); throw error; } finally { // Always capture run duration, even on abort/error, so we can alert on - // long runs where the user gave up before completion. - if (!receivedSuccessResult) { + // long runs where the user gave up before completion. A 401 never reached + // this block before (the process exited first), so it still does not count. + if (!receivedSuccessResult && !authFailure) { const durationMs = Date.now() - startTime; analytics.wizardCapture('agent aborted', { duration_ms: durationMs, @@ -1737,6 +1783,7 @@ function handleSDKMessage( options: WizardRunOptions, spinner: SpinnerHandle, signals: AgentOutputSignals, + emit: ProgressEmitter, receivedSuccessResult = false, tasks?: Map, // The orchestrator owns the TUI task panel (it renders its queue). Suppress the @@ -1762,7 +1809,7 @@ function handleSDKMessage( const sorted = Array.from(tasks.values()).sort( (a, b) => rank(a.status) - rank(b.status), ); - getUI().syncTodos(sorted); + emit({ kind: 'tasks', tasks: sorted.map((t) => ({ ...t })) }); }; logToFile(`SDK Message: ${message.type}`, JSON.stringify(message, null, 2)); @@ -1777,7 +1824,7 @@ function handleSDKMessage( // dedup for SDK-retried turns — see addTokenUsage's doc comment), so // this stays live-updating for every run, not just `--benchmark`. const tokenUsageDelta = extractTokenUsageDelta(message); - if (tokenUsageDelta) getUI().addTokenUsage(tokenUsageDelta); + if (tokenUsageDelta) emit({ kind: 'usage', delta: tokenUsageDelta }); // Extract text content from assistant messages const content = message.message?.content; @@ -1797,7 +1844,7 @@ function handleSDKMessage( const statusMatch = block.text.match(statusRegex); if (statusMatch) { const statusText = statusMatch[1].trim(); - getUI().pushStatus(statusText); + emit({ kind: 'status', message: statusText }); spinner.message(statusText); } @@ -1811,7 +1858,11 @@ function handleSDKMessage( ); const dashboardMatch = block.text.match(dashboardRegex); if (dashboardMatch) { - getUI().setDashboardUrl(dashboardMatch[1].trim()); + emit({ + kind: 'url', + which: 'dashboard', + url: dashboardMatch[1].trim(), + }); } // Check for [NOTEBOOK_URL] markers @@ -1824,7 +1875,11 @@ function handleSDKMessage( ); const notebookMatch = block.text.match(notebookRegex); if (notebookMatch) { - getUI().setNotebookUrl(notebookMatch[1].trim()); + emit({ + kind: 'url', + which: 'notebook', + url: notebookMatch[1].trim(), + }); } } @@ -1847,7 +1902,7 @@ function handleSDKMessage( // Mirror the active tool into the Visualizer's "stage" indicator. if (block.type === 'tool_use') { const stage = classifyToolToStage((block as ToolUseBlock).name); - if (stage) getUI().setStage(stage); + if (stage) emit({ kind: 'stage', stage }); } } } @@ -1900,7 +1955,7 @@ function handleSDKMessage( // mode race conditions). Full message already logged above via JSON dump. if (message.errors && !receivedSuccessResult) { for (const err of message.errors) { - getUI().log.error(`Error: ${err}`); + emit({ kind: 'log', level: 'error', message: `Error: ${err}` }); logToFile('ERROR:', err); } } @@ -1915,7 +1970,7 @@ function handleSDKMessage( // Full message already logged above via JSON dump. if (message.errors && !receivedSuccessResult) { for (const err of message.errors) { - getUI().log.error(`Error: ${err}`); + emit({ kind: 'log', level: 'error', message: `Error: ${err}` }); logToFile('ERROR:', err); } } diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts index 8b92c4ebe..3adf72fd9 100644 --- a/src/lib/agent/agent-runner.ts +++ b/src/lib/agent/agent-runner.ts @@ -1,11 +1,12 @@ /** * Re-export shim. The runner has been split into agent/runner/. * Import from there directly; this shim keeps existing importers working. + * The session-driven `runAgent(programConfig, session)` lives in + * `src/lib/programs/run-agent-legacy.ts`. */ export { runAgent, - runProgram, shouldDisableAsk, type ProgramRun, type BootstrapResult, diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts new file mode 100644 index 000000000..a85820dd3 --- /dev/null +++ b/src/lib/agent/progress.ts @@ -0,0 +1,100 @@ +/** + * The agent's public progress and interaction contracts. + * + * `runAgent` reports through one optional callback and asks through one + * optional set of capabilities. Neither reaches into a UI singleton, a store, + * or a session: every payload is copied data, every question is awaited on an + * injected answerer. The legacy adapter in `src/lib/programs/run-agent-legacy.ts` + * maps these back onto `WizardUI` one call per event, so the terminal output of + * every existing runner is unchanged. + */ + +import type { + AskAnswers, + OutroData, + PendingQuestion, + TaskNotice, +} from '@lib/wizard-session'; +import type { + AuthErrorDetail, + SpinnerHandle, + TokenUsageDelta, +} from '@ui/wizard-ui'; + +export type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta }; + +/** One task as the host renders it. The same shape `WizardUI.syncTodos` takes. */ +export interface TaskSnapshot { + content: string; + status: string; + activeForm?: string; +} + +export type ProgressLogLevel = 'info' | 'warn' | 'error' | 'success' | 'step'; + +/** + * Everything the agent reports while it runs. One event per former + * `getUI()` call, in the same order, with the same payload, so a reducer that + * maps each case back onto `WizardUI` reproduces today's output exactly. + * + * Payloads are copies. Never a store, a setter, a function or a live + * collection. The callback returns nothing and the agent never branches on it. + */ +export type AgentProgress = + /** The run's main work has started (`WizardUI.startRun`). */ + | { kind: 'lifecycle'; phase: 'started' } + /** The run finished and the host may show its outro (`WizardUI.outro`). */ + | { kind: 'lifecycle'; phase: 'completed'; message: string } + /** The run spinner (`WizardUI.spinner()`), one handle per run. */ + | { + kind: 'spinner'; + action: 'start' | 'stop' | 'message'; + message?: string; + } + /** A log line (`WizardUI.log[level]`). */ + | { kind: 'log'; level: ProgressLogLevel; message: string } + /** A `[STATUS]` line the agent printed (`WizardUI.pushStatus`). */ + | { kind: 'status'; message: string } + /** The full task list, already sorted for display (`WizardUI.syncTodos`). */ + | { kind: 'tasks'; tasks: TaskSnapshot[] } + /** The stage of work derived from the active tool (`WizardUI.setStage`). */ + | { kind: 'stage'; stage: string } + /** A PostHog URL the agent created (`setDashboardUrl` / `setNotebookUrl`). */ + | { kind: 'url'; which: 'dashboard' | 'notebook'; url: string } + /** One assistant turn's token usage (`WizardUI.addTokenUsage`). */ + | { kind: 'usage'; delta: TokenUsageDelta } + /** The SDK's authoritative run cost (`WizardUI.setFinalTokenCostUsd`). */ + | { kind: 'finalCost'; usd: number } + /** The gateway returned 401; a failure follows (`WizardUI.showAuthError`). */ + | { kind: 'authError'; detail: AuthErrorDetail } + /** The run's final outro payload (`WizardUI.setOutroData`). */ + | { kind: 'completion'; outro: OutroData }; + +export type ProgressEmitter = (event: AgentProgress) => void; + +/** + * The questions the agent may need a person (or a script) to answer. Every + * capability is optional. With none supplied the agent installs no ask bridge, + * so `wizard_ask` returns its existing "not available" error, and an optional + * task notice is declined — the same path a `--ci` run takes today. + */ +export interface AgentInteraction { + /** + * Open a question and resolve with the answers. The bridge that calls this + * owns the timeout, the `__cancelled__` sentinel and the analytics; `signal` + * aborts when the run is cancelled so the host can dismiss its overlay. + */ + ask?: ( + question: PendingQuestion, + context: { signal: AbortSignal }, + ) => Promise; + /** Dismiss the in-flight question as cancelled (timeouts call this). */ + cancelAsk?: () => void; + /** Offer an optional step and resolve with whether to keep it. */ + taskNotice?: ( + notice: TaskNotice, + context: { signal: AbortSignal }, + ) => Promise; + /** Dismiss an in-flight task notice as declined (timeouts call this). */ + cancelTaskNotice?: () => void; +} diff --git a/src/lib/agent/runner/README.md b/src/lib/agent/runner/README.md index 86fee7f1b..b9f9bde83 100644 --- a/src/lib/agent/runner/README.md +++ b/src/lib/agent/runner/README.md @@ -39,19 +39,43 @@ for the coordinated change checklist. Five layers, each with its own job. Nothing crosses layers unless it has to. -**The entry point** (`index.ts`) is the front door. It receives a program config -and a session, and orchestrates the run at the highest level. +**The entry point** (`index.ts`) is the front door: + +```ts +runAgent(config: RunConfig, input: RunInput, options?: { + onProgress?: (event: AgentProgress) => void; + interaction?: AgentInteraction; + signal?: AbortSignal; +}): Promise +``` -**Bootstrap** (`shared/bootstrap.ts`) is the on-ramp. Every run starts with the -same setup work — health checks, settings conflicts, OAuth, PostHog feature flag -fetch, MCP URL, AI opt-in gate. Whether the run turns out to be linear or -orchestrator, anthropic or pi, the setup is the same. +`RunConfig` is resolved execution data: the program id, its run definition, the +binding the caller resolved, flags, run tags and tool lists. `RunInput` is the +invocation snapshot: directory, credentials, project, skill. The agent reports +through `onProgress` (one event per former UI call, copied data, never awaited) +and asks through `interaction` (an optional answerer for `wizard_ask` and task +notices). It returns a `RunResult` — outcome, outro, a failure with the same +fields `wizardAbort` takes, and a snapshot of what it reported. It never +renders, never reads a session, never exits the process and never rejects: a +decided failure is `aborted` or `failed`, an error the agent did not decide is +`crashed` with the original error attached. Types live in `shared/types.ts` and +`../progress.ts`. + +Everything the caller decides first — health and settings gates, OAuth, the AI +opt-in gate, post-auth gates, feature flags, token refresh, the binding — lives +in `src/lib/programs/run-agent-legacy.ts`, which also maps progress back onto +`getUI()` for today's runners. + +**Prepare** (`shared/bootstrap.ts`) is the on-ramp inside the agent: logging +targets, the gateway mint and the scan-triage classifier. Whether the run turns +out to be linear or orchestrator, anthropic or pi, the setup is the same. **The switchboard** (`switchboard/`) is the router. Given a program id + the fetched flags + any CLI overrides, it returns a `ProgramBinding` — which query shape (sequence), which agent SDK (harness), which model. Two independent middleware chains, one per axis, apply precedence rules (CLI > flag > program -config > default). This is the only layer that makes routing decisions. +config > default). The caller resolves the run-level binding; the orchestrator +re-resolves the harness per task role from the same inputs. **Sequences** (`sequence/`) are LLM query shapes. Once the switchboard has picked one, that sequence takes over the run and owns _how the LLM's work is @@ -72,8 +96,7 @@ gateway. ## How they connect -- Bootstrap prepares shared services, including triage selected for the resolved - harness. +- Prepare mints the gateway token and builds triage for the resolved harness. - The switchboard knows which sequences and harnesses exist (via its two registries), but not what they do. - A sequence knows how to shape a conversation, but delegates the actual model @@ -84,12 +107,13 @@ Each layer is replaceable. ## Flow -1. Program picked → session built. -2. Bootstrap runs (shared setup, fetches PostHog flags). -3. Switchboard resolves a `ProgramBinding { sequence, harness, model }`. -4. Analytics tags the run with its bindings. -5. Sequence takes over — shapes the LLM's work into one conversation (linear) or - many (orchestrator). -6. Harness drives each conversation through its SDK, using the bound model, on +1. The caller runs its gates, authenticates, fetches PostHog flags and resolves + a `ProgramBinding { sequence, harness, model }`; analytics tags the run. +2. `runAgent(config, input, options)` prepares (mint, triage). +3. Sequence takes over — shapes the LLM's work into one conversation (linear) or + many (orchestrator), reporting through `onProgress`. +4. Harness drives each conversation through its SDK, using the bound model, on the PostHog LLM gateway. -7. Cleanup runs (scan report, settings restore, outro). +5. The scan report flushes; `runAgent` returns a `RunResult`. +6. The caller applies it: an outro is already reported, a decided failure goes + to `wizardAbort`, a crash is rethrown for the runner's own handling. diff --git a/src/lib/agent/runner/harness/anthropic/index.ts b/src/lib/agent/runner/harness/anthropic/index.ts index 4f5e449ec..342175f8c 100644 --- a/src/lib/agent/runner/harness/anthropic/index.ts +++ b/src/lib/agent/runner/harness/anthropic/index.ts @@ -1,6 +1,5 @@ // Supported legacy SDK fallback; both this adapter and Pi implement run and runTask. -import { getUI } from '@ui'; import { Harness } from '@lib/constants'; import { initializeAgent, @@ -9,7 +8,8 @@ import { import { createAioCapture } from '@lib/agent/aio-capture'; import { getLogFilePath, logToFile } from '@utils/debug'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { sessionToOptions } from '@lib/agent/runner/shared/bootstrap'; +import { runOptions } from '@lib/agent/runner/shared/bootstrap'; +import { createEmitLog } from '@lib/agent/runner/shared/progress-collector'; import type { AgentResult, AgentHarness, @@ -22,30 +22,32 @@ export const anthropicBackend: AgentHarness = { async run(inputs: BackendRunInputs): Promise { const { - session, - config, - programConfig, + config: runConfig, + input, boot, + emit, prompt, spinner, askBridge, middleware, model, } = inputs; + const config = runConfig.run; const { skillsBaseUrl, credentials, wizardFlags, wizardMetadata } = boot; const { accessToken, host, projectApiKey } = credentials; + const log = createEmitLog(emit); const capture = createAioCapture({ - enabled: session.captureAio, + enabled: input.flags.captureAio, projectApiKey, apiHost: host.apiHost, runTags: wizardMetadata, }); - getUI().log.step('Initializing Claude agent...'); + log.step('Initializing Claude agent...'); const agent = await initializeAgent( { - workingDirectory: session.installDir, + workingDirectory: input.installDir, posthogMcpUrl: host.mcpUrl, posthogApiKey: accessToken, host, @@ -59,22 +61,22 @@ export const anthropicBackend: AgentHarness = { integrationLabel: config.integrationLabel, askBridge, askMaxQuestions: config.maxQuestions, - allowedTools: programConfig.allowedTools, - disallowedTools: programConfig.disallowedTools, - getPendingQuestion: () => session.pendingQuestion, + allowedTools: runConfig.allowedTools, + disallowedTools: runConfig.disallowedTools, modelOverride: model, capture, + emit, }, - sessionToOptions(session), + runOptions(input), ); - getUI().log.step(`Verbose logs: ${getLogFilePath()}`); - getUI().log.success("Agent initialized. Let's get cooking!"); + log.step(`Verbose logs: ${getLogFilePath()}`); + log.success("Agent initialized. Let's get cooking!"); logToFile('[agent-runner] agent initialized'); return executeAgent( agent, prompt, - sessionToOptions(session), + runOptions(input), spinner, { estimatedDurationMinutes: config.estimatedDurationMinutes, @@ -94,9 +96,10 @@ export const anthropicBackend: AgentHarness = { async runTask(inputs: TaskRunInputs): Promise { const { - session, - programConfig, + config, + input, boot, + emit, prompt, spinner, model, @@ -111,10 +114,10 @@ export const anthropicBackend: AgentHarness = { requestRemark, analyticsProperties, } = inputs; - const options = sessionToOptions(session); + const options = runOptions(input); const capture = createAioCapture({ - enabled: session.captureAio, + enabled: input.flags.captureAio, projectApiKey: boot.credentials.projectApiKey, apiHost: boot.credentials.host.apiHost, runTags: boot.wizardMetadata, @@ -125,7 +128,7 @@ export const anthropicBackend: AgentHarness = { // enqueue_task attribute to the right agent when tasks run in parallel. const agent = await initializeAgent( { - workingDirectory: session.installDir, + workingDirectory: input.installDir, posthogMcpUrl: boot.credentials.host.mcpUrl, posthogApiKey: boot.credentials.accessToken, host: boot.credentials.host, @@ -134,12 +137,13 @@ export const anthropicBackend: AgentHarness = { programId: boot.programId, wizardFlags: boot.wizardFlags, wizardMetadata: boot.wizardMetadata, - integrationLabel: programConfig.id, + integrationLabel: config.programId, // Only a task allowed to ask carries a bridge, so the Write/Edit pause // that rides on a pending question stays inside that task's agent. askBridge, orchestrator, capture, + emit, }, options, ); diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index 092e59718..82c705615 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -14,7 +14,6 @@ import fs from 'fs'; import path from 'path'; -import { getUI } from '@ui'; import { getLogFilePath, logToFile } from '@utils/debug'; import { Harness, @@ -41,6 +40,8 @@ import type { TaskRunInputs, } from '../types'; import type { BootstrapResult } from '@lib/agent/runner/shared/types'; +import type { ProgressEmitter } from '@lib/agent/progress'; +import { createEmitLog } from '@lib/agent/runner/shared/progress-collector'; import type { TaskStore } from './tasks'; import { completionFailure, runErrorType } from './completion'; @@ -147,10 +148,19 @@ export function extractText(message: unknown): string { * the MCP creates them) into the outro link, mirroring the anthropic path's * signal parsing (#9). The marker carries the URL the MCP returned. */ -export function applyOutroMarkers(textBlock: string): void { +export function applyOutroMarkers( + textBlock: string, + emit: ProgressEmitter, +): void { const markers: Array<[string, (url: string) => void]> = [ - [AgentSignals.DASHBOARD_URL, (url) => getUI().setDashboardUrl(url)], - [AgentSignals.NOTEBOOK_URL, (url) => getUI().setNotebookUrl(url)], + [ + AgentSignals.DASHBOARD_URL, + (url) => emit({ kind: 'url', which: 'dashboard', url }), + ], + [ + AgentSignals.NOTEBOOK_URL, + (url) => emit({ kind: 'url', which: 'notebook', url }), + ], ]; for (const [marker, apply] of markers) { const idx = textBlock.indexOf(marker); @@ -195,20 +205,22 @@ export const piBackend: AgentHarness = { name: Harness.pi, async run(inputs: BackendRunInputs): Promise { - const { session, boot, prompt, spinner, config, programConfig } = inputs; + const { config: runConfig, input, boot, emit, prompt, spinner } = inputs; + const config = runConfig.run; const modelId = inputs.model; + const log = createEmitLog(emit); const capture = createAioCapture({ - enabled: session.captureAio, + enabled: input.flags.captureAio, projectApiKey: boot.credentials.projectApiKey, apiHost: boot.credentials.host.apiHost, runTags: boot.wizardMetadata, }); // Init banner (parity #5). - getUI().log.step('Initializing Wizard agent...'); - getUI().log.step(`Verbose logs: ${getLogFilePath()}`); - getUI().log.success("Agent initialized. Let's get cooking!"); + log.step('Initializing Wizard agent...'); + log.step(`Verbose logs: ${getLogFilePath()}`); + log.success("Agent initialized. Let's get cooking!"); spinner.start(config.spinnerMessage ?? 'Customizing your PostHog setup...'); @@ -306,11 +318,11 @@ export const piBackend: AgentHarness = { const { createSecurityExtension } = await import('./security'); const security = createSecurityExtension({ - disallowedTools: programConfig.disallowedTools, + disallowedTools: runConfig.disallowedTools, getWizardAskPending: () => askState.pending, triageProvider: boot.triageProvider, // Where pi's bash runs; the rm allowance is confined to this tree. - workingDirectory: session.installDir, + workingDirectory: input.installDir, }); // Pay warlock's WASM-init + rule-compile cost now, off the tool-call @@ -360,11 +372,11 @@ export const piBackend: AgentHarness = { } const resourceLoader = new DefaultResourceLoader({ - cwd: session.installDir, + cwd: input.installDir, agentDir: getAgentDir(), systemPrompt: assembleCommandments({ - program: programConfig.id, + program: runConfig.programId, sequence: Sequence.linear, harness: Harness.pi, caps: { bash: true, posthogMcp }, @@ -390,12 +402,14 @@ export const piBackend: AgentHarness = { const { createWizardPiTaskTools } = await import('./tasks'); const { createDispatchAgentTool } = await import('./subagent'); // Created once so the run loop can read the store for the completion guard. - const wizardTaskTools = createWizardPiTaskTools(); + const wizardTaskTools = createWizardPiTaskTools((tasks) => + emit({ kind: 'tasks', tasks }), + ); // The one bash the agent (and its subagents) may use: every subprocess it // spawns gets a scrubbed env, so no secret or ambient variable reaches an // `npm install`. Shared with the subagent so the lockdown is inherited. const scrubbedBash = withMode( - createBashToolDefinition(session.installDir, { + createBashToolDefinition(input.installDir, { spawnHook: (ctx) => ({ ...ctx, env: buildScrubbedEnv() }), }), 'sequential', @@ -406,18 +420,18 @@ export const piBackend: AgentHarness = { // defaults so we can supply the env-scrubbed bash above; read/edit/write // are the stock definitions. Reads run in parallel so a batched turn of // independent reads executes at once; edit/write/bash stay sequential. - withMode(createReadToolDefinition(session.installDir), 'parallel'), - withMode(createEditToolDefinition(session.installDir), 'sequential'), - withMode(createWriteToolDefinition(session.installDir), 'sequential'), + withMode(createReadToolDefinition(input.installDir), 'parallel'), + withMode(createEditToolDefinition(input.installDir), 'sequential'), + withMode(createWriteToolDefinition(input.installDir), 'sequential'), scrubbedBash, // Native ls/find/grep so the agent explores with proper tools instead // of fence-blocked `bash {ls/find}` (the profiled retry-spirals came // from this gap). Parallel — exploration batches cleanly. - withMode(createLsToolDefinition(session.installDir), 'parallel'), - withMode(createFindToolDefinition(session.installDir), 'parallel'), - withMode(createGrepToolDefinition(session.installDir), 'parallel'), + withMode(createLsToolDefinition(input.installDir), 'parallel'), + withMode(createFindToolDefinition(input.installDir), 'parallel'), + withMode(createGrepToolDefinition(input.installDir), 'parallel'), ...createWizardPiTools({ - workingDirectory: session.installDir, + workingDirectory: input.installDir, skillsBaseUrl: boot.skillsBaseUrl, triageProvider: boot.triageProvider, detectPackageManager: config.detectPackageManager, @@ -431,7 +445,7 @@ export const piBackend: AgentHarness = { }, // Skip wizard_ask when the program disallows it (bare pi tool names // don't match the MCP-prefixed disallow list at the security gate). - disallowedTools: programConfig.disallowedTools, + disallowedTools: runConfig.disallowedTools, }), // Task/todo tools (#526): render the todo list live in the TUI, parity // with the anthropic path. @@ -442,7 +456,7 @@ export const piBackend: AgentHarness = { createDispatchAgentTool({ model, modelRegistry: registry, - cwd: session.installDir, + cwd: input.installDir, agentDir: getAgentDir(), securityFactory: security.factory as (pi: unknown) => void, bashTool: scrubbedBash, @@ -456,8 +470,8 @@ export const piBackend: AgentHarness = { // Reasoning effort from the switchboard capability matrix (undefined = // pi's default). Sent as `reasoning_effort` for openai-completions. thinkingLevel: caps.thinkingLevel, - cwd: session.installDir, - sessionManager: SessionManager.inMemory(session.installDir), + cwd: input.installDir, + sessionManager: SessionManager.inMemory(input.installDir), resourceLoader, // Disable the default built-in tools; `customTools` re-registers // read/edit/write + an env-scrubbed bash, so no subprocess inherits the @@ -508,12 +522,12 @@ export const piBackend: AgentHarness = { const assistant = extractText(event.message).trim(); if (assistant) { logToFile(`[pi] assistant: ${assistant.slice(0, 1000)}`); - applyOutroMarkers(assistant); + applyOutroMarkers(assistant, emit); // Surface [STATUS] lines into the live spinner + status history, // mirroring the anthropic path — pi otherwise drops them. const statusText = lastStatusLine(assistant); if (statusText) { - getUI().pushStatus(statusText); + emit({ kind: 'status', message: statusText }); spinner.message(statusText); } for (const line of assistant.split('\n')) signals.push(line); @@ -644,7 +658,7 @@ export const piBackend: AgentHarness = { // on completion; pi's `rm` is fence-blocked, so the agent can't — clean it // up host-side rather than leave a stale (often empty) artifact (#15). try { - const planFile = path.join(session.installDir, '.posthog-events.json'); + const planFile = path.join(input.installDir, '.posthog-events.json'); if (fs.existsSync(planFile)) await fs.promises.rm(planFile); } catch (err) { logToFile(`[pi] .posthog-events.json cleanup skipped: ${String(err)}`); @@ -668,7 +682,7 @@ export const piBackend: AgentHarness = { const message = err instanceof Error ? err.message : String(err); logToFile(`[pi] run error: ${message}`); spinner.stop(config.errorMessage ?? `${config.integrationLabel} failed`); - getUI().log.error(`pi backend error: ${message}`); + log.error(`pi backend error: ${message}`); const error = runErrorType(message); captureAborted(error); return { error, message }; diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 37deec62d..9d7adc97c 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -16,7 +16,6 @@ * Loaded lazily from `index.ts` (typebox/ESM constraint, same as tools.ts). */ -import { getUI } from '@ui'; import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; import { @@ -166,9 +165,10 @@ function isSettled(ctx: OrchestratorToolsContext): boolean { export async function runPiTask(inputs: TaskRunInputs): Promise { const { - session, - programConfig, + config, + input, boot, + emit, prompt, spinner, model: modelId, @@ -187,7 +187,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { if (spinnerMessage) spinner.start(spinnerMessage); const capture = createAioCapture({ - enabled: session.captureAio, + enabled: input.flags.captureAio, projectApiKey: boot.credentials.projectApiKey, apiHost: boot.credentials.host.apiHost, runTags: boot.wizardMetadata, @@ -319,10 +319,10 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { const orchestratorTools = allowedOrchestratorTools(disallowedTools); const resourceLoader = new DefaultResourceLoader({ - cwd: session.installDir, + cwd: input.installDir, agentDir: getAgentDir(), systemPrompt: assembleCommandments({ - program: programConfig.id, + program: config.programId, sequence: Sequence.orchestrator, harness: Harness.pi, caps: { bash: codingTools.has('bash'), posthogMcp }, @@ -339,7 +339,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { // The task's coding tools, gated by its allow list. Reads and searches run // in parallel; mutating tools stay sequential. Bash subprocesses get the // scrubbed env, same as the linear run. - const dir = session.installDir; + const dir = input.installDir; const codingToolFactories = { read: () => withMode(createReadToolDefinition(dir), 'parallel'), edit: () => withMode(createEditToolDefinition(dir), 'sequential'), @@ -436,10 +436,10 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { const assistant = extractText(event.message).trim(); if (assistant) { logToFile(`[pi-task] assistant: ${assistant.slice(0, 1000)}`); - applyOutroMarkers(assistant); + applyOutroMarkers(assistant, emit); const statusText = lastStatusLine(assistant); if (statusText) { - getUI().pushStatus(statusText); + emit({ kind: 'status', message: statusText }); spinner.message(statusText); } for (const line of assistant.split('\n')) signals.push(line); diff --git a/src/lib/agent/runner/harness/pi/tasks.ts b/src/lib/agent/runner/harness/pi/tasks.ts index e12f66e1e..0ee0f4462 100644 --- a/src/lib/agent/runner/harness/pi/tasks.ts +++ b/src/lib/agent/runner/harness/pi/tasks.ts @@ -1,15 +1,15 @@ /** * Task/todo parity for pi (#526). The same four Task tools the anthropic path * exposes (TaskCreate/Update/Get/List), as pi `defineTool` tools backed by a - * shared in-memory store. Every mutation pushes the list to the TUI via - * `getUI().syncTodos`, so the todo panel updates live under pi exactly like the - * anthropic path — the thing that was missing before. + * shared in-memory store. Every mutation reports the list through `onSync` + * (a `tasks` progress event), so the todo panel updates live under pi exactly + * like the anthropic path — the thing that was missing before. */ import { Type } from 'typebox'; import { defineTool } from '@earendil-works/pi-coding-agent'; import type { ToolDefinition } from '@earendil-works/pi-coding-agent'; -import { getUI } from '@ui'; +import type { TaskSnapshot } from '@lib/agent/progress'; export type TaskStatus = 'pending' | 'in_progress' | 'completed'; export interface TaskEntry { @@ -26,22 +26,23 @@ function text(s: string): { return { content: [{ type: 'text', text: s }], details: {} }; } -function syncToTui(store: TaskStore): void { - getUI().syncTodos( - Array.from(store.values()).map((t) => ({ - content: t.content, - status: t.status, - activeForm: t.activeForm, - })), - ); +function snapshot(store: TaskStore): TaskSnapshot[] { + return Array.from(store.values()).map((t) => ({ + content: t.content, + status: t.status, + activeForm: t.activeForm, + })); } -/** Build the four Task tools over a fresh store. */ -export function createWizardPiTaskTools(): { +/** Build the four Task tools over a fresh store. `onSync` gets the list after every mutation. */ +export function createWizardPiTaskTools( + onSync: (tasks: TaskSnapshot[]) => void = () => undefined, +): { tools: ToolDefinition[]; store: TaskStore; } { const store: TaskStore = new Map(); + const syncToTui = (): void => onSync(snapshot(store)); const taskCreate = defineTool({ name: 'TaskCreate', @@ -64,7 +65,7 @@ export function createWizardPiTaskTools(): { status: 'pending', activeForm: args.activeForm, }); - syncToTui(store); + syncToTui(); return text(`Created ${id}`); }, }); @@ -97,7 +98,7 @@ export function createWizardPiTaskTools(): { status: (args.status as TaskStatus) ?? existing.status, activeForm: args.activeForm ?? existing.activeForm, }); - syncToTui(store); + syncToTui(); return text(`Updated ${args.taskId}`); }, }); diff --git a/src/lib/agent/runner/harness/types.ts b/src/lib/agent/runner/harness/types.ts index 67c59e5c9..35c082c21 100644 --- a/src/lib/agent/runner/harness/types.ts +++ b/src/lib/agent/runner/harness/types.ts @@ -13,23 +13,27 @@ * per drained task. A harness without orchestrator support omits the method; * `orchestrator-runner.ts` checks for it at the call site and fails loudly * rather than silently downgrading. + * + * A harness reports through `emit` and never reaches for a UI. It returns an + * error classification, or a decided `failure` the sequence must return as + * the run's result. */ -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 { SpinnerHandle } from '@ui'; import type { WizardAskBridge } from '@lib/wizard-ask-bridge'; import type { AgentErrorType } from '@lib/agent/agent-interface'; +import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; import type { OrchestratorToolsContext } from '@lib/agent/runner/sequence/orchestrator/queue-tools'; import type { EffortLevel, ThinkingLevel, } from '@lib/agent/runner/switchboard/models'; import type { - ProgramRun, + AgentFailure, BootstrapResult, + RunConfig, + RunInput, } from '@lib/agent/runner/shared/types'; /** The benchmark/telemetry hook threaded through a run, if enabled. */ @@ -40,14 +44,14 @@ export interface RunMiddleware { /** * Everything a runner needs to run one program. Assembled by `linear.ts` from - * the bootstrap result and the program config; the runner consumes it and never + * the prepared run and the run config; the runner consumes it and never * re-derives run context. */ export interface BackendRunInputs { - session: WizardSession; - config: ProgramRun; - programConfig: ProgramConfig; + config: RunConfig; + input: RunInput; boot: BootstrapResult; + emit: ProgressEmitter; /** The fully assembled prompt. */ prompt: string; /** Installed framework-skill path, when the program installs one. */ @@ -56,7 +60,7 @@ export interface BackendRunInputs { spinner: SpinnerHandle; /** Interactive question bridge; undefined in CI/headless (ask disabled). */ askBridge?: WizardAskBridge; - /** Benchmark middleware, when `session.benchmark` is set. */ + /** Benchmark middleware, when `--benchmark` is set. */ middleware?: RunMiddleware; /** Gateway model id resolved from the (runner, model) pair. */ model: string; @@ -64,8 +68,16 @@ export interface BackendRunInputs { thinkingLevel?: EffortLevel; } -/** What a runner reports back: an error classification, or nothing on success. */ -export type AgentResult = { error?: AgentErrorType; message?: string }; +/** + * What a runner reports back: an error classification, or nothing on success. + * `failure` is a fully decided abort the harness already reported to the host + * (the 401 auth screen); the sequence returns it as the run's result. + */ +export type AgentResult = { + error?: AgentErrorType; + message?: string; + failure?: AgentFailure; +}; /** * One orchestrator-mode unit of work — the seed plan, or one drained task. @@ -75,9 +87,10 @@ export type AgentResult = { error?: AgentErrorType; message?: string }; * them from the program-level config the linear pipeline assembles once. */ export interface TaskRunInputs { - session: WizardSession; - programConfig: ProgramConfig; + config: RunConfig; + input: RunInput; boot: BootstrapResult; + emit: ProgressEmitter; /** The fully assembled per-task or seed prompt. */ prompt: string; spinner: SpinnerHandle; diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index f11bf73c8..6c60112aa 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -1,214 +1,129 @@ /** * Unified program runner — dispatcher. * - * Single configurable pipeline for all programs. Each program - * provides a ProgramRun (via the `run` field on ProgramConfig) - * that controls: - * - Whether a skill is pre-installed or discovered at runtime - * - How the agent prompt is built - * - What MCP servers and package manager detector to use - * - What happens after the agent completes + * One callable, `runAgent(config, input, options)`, runs a program's agent + * pipeline to a decided result. `config` is resolved execution data (which + * program, how it is routed, which flags apply); `input` is the invocation + * snapshot (directory, credentials, project); `options` carries an optional + * progress observer, an optional answerer and an optional cancel signal. * - * The pipeline runs a shared bootstrap (logging, health check, settings, OAuth, - * flags, MCP url), then forks. The `orchestrator` variant routes to the - * experimental task-queue runner. Every other variant runs the fixed linear - * pipeline: + * The pipeline prepares the run (logging targets, gateway mint, scan triage), + * then forks. The `orchestrator` variant routes to the task-queue runner. + * Every other variant runs the fixed linear pipeline: * [skill install] → agent init → prompt → run → errors → [postRun] → outro + * + * The agent reports and asks, it never renders, never reads a session, never + * exits the process and never rejects. A decided failure comes back in + * `RunResult.failure` with the same fields `wizardAbort` takes; an error the + * agent did not decide (a refused mint, an SDK crash) comes back as + * `outcome: 'crashed'` with the original error attached, so a caller can keep + * handling it the way it always did. The legacy adapter in + * `src/lib/programs/run-agent-legacy.ts` rebuilds today's session-driven + * behavior on top of this call for every existing caller. */ -import type { WizardSession } from '../../wizard-session'; -import { analytics } from '@utils/analytics'; -import { - Sequence, - WIZARD_ORCHESTRATOR_FLAG_KEY, - WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, -} from '@lib/constants'; +import { Sequence } from '@lib/constants'; +import { classifyRunFailure } from '@lib/errors'; import { logToFile } from '@utils/debug'; -import { getUI } from '../../../ui'; -import type { ProgramConfig } from '../../programs/program-step'; -import type { ProgramRun, BootstrapResult } from './shared/types'; -import { bootstrapProgram } from './shared/bootstrap'; -import { - getSequence, - resolveBinding, - type ProgramBinding, - type SwitchboardCtx, -} from './switchboard'; +import type { + RunAgentOptions, + RunConfig, + RunInput, + RunResult, +} from './shared/types'; +import { prepareRun } from './shared/bootstrap'; +import { createProgressCollector } from './shared/progress-collector'; +import { getSequence } from './switchboard'; import { flushScanReport } from '../../yara-hooks'; -import { startAuditLedgerWatcher } from '../../programs/audit/ledger-watcher'; -import { registerCleanup } from '../../../utils/wizard-abort'; export type { - ProgramRun, - BootstrapResult, AbortCase, - PromptContext, + AgentFailure, + BootstrapResult, Credentials, + ProgramRun, + PromptContext, + ResolvedBinding, + RunAgentOptions, + RunConfig, + RunFlags, + RunHooks, + RunInput, + RunOutcome, + RunResult, + RunSnapshot, + SeedTaskEntry, } from './shared/types'; +export type { + AgentInteraction, + AgentProgress, + ProgressEmitter, +} from '@lib/agent/progress'; export { shouldDisableAsk } from './shared/bootstrap'; - -/** - * Resolve a ProgramConfig'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, - session: WizardSession, - options: { composed?: boolean } = {}, -): Promise { - if (!programConfig.run) { - throw new Error(`Program "${programConfig.id}" has no run configuration.`); - } - - // Before `run()` resolves: an audit seeds the ledger from inside its recipe, - // and a watcher started later would ignore that write as pre-existing. - const ledger = programConfig.auditLedgerFile - ? startAuditLedgerWatcher(session.installDir, programConfig.auditLedgerFile) - : null; - if (ledger) registerCleanup(() => ledger.stop()); - - try { - const runDef = - typeof programConfig.run === 'function' - ? await programConfig.run(session) - : programConfig.run; - - await runProgram(session, runDef, programConfig, options); - } finally { - ledger?.stop(); - } -} +export { resolveBinding } from './switchboard'; +export type { ProgramBinding, SwitchboardCtx } from './switchboard'; /** * Run a program's agent pipeline. * - * Bootstrap → bind the program via the switchboard (resolve which sequence - * and harness will run it, tag both axes) → dispatch to the resolved - * sequence's runner. + * Prepare → dispatch to the sequence the binding names → return its result + * with the agent's own snapshot of what it reported. Missing observers change + * nothing; a throwing observer is logged and the run continues. */ -export async function runProgram( - session: WizardSession, - config: ProgramRun, - programConfig: ProgramConfig, - options: { composed?: boolean } = {}, -): Promise { - const boot = await bootstrapProgram(session, config, programConfig); +export async function runAgent( + config: RunConfig, + input: RunInput, + options: RunAgentOptions = {}, +): Promise { + const collector = createProgressCollector(options.onProgress); + const { emit } = collector; + const signal = options.signal ?? new AbortController().signal; + const log = (message: string) => + emit({ kind: 'log', level: 'info', message }); // Flush the warlock scan report once, at this single seam, on every - // termination path and for every harness (linear, orchestrator, or future): - // - registerCleanup covers the abort/cancel path (wizardAbort runs the - // registered cleanups; the success path never calls them) - // - the finally covers normal completion and any direct throw that unwinds - // through here - // flushScanReport is idempotent (it zeroes scan state), so the overlap is a - // harmless no-op. No harness has to know reporting exists. - registerCleanup(() => flushScanReport(session)); + // termination path and for every harness (linear, orchestrator, or future). + // flushScanReport is idempotent (it zeroes scan state), so a caller that also + // flushes from its own cleanup path sees a harmless no-op. No harness has to + // know reporting exists. try { - const binding = resolveProgramRunner( - session, - programConfig, - boot, - options.composed ?? false, - ); - if (binding.sequence === Sequence.orchestrator) { - getUI().log.info('Task-queue orchestrator enabled.'); + const boot = await prepareRun(config, input); + if (config.binding.sequence === Sequence.orchestrator) { + log('Task-queue orchestrator enabled.'); } - return await getSequence(binding.sequence).run( - session, + logToFile( + `[agent-runner] run program=${config.programId} sequence=${config.binding.sequence}` + + ` harness=${config.binding.harness} composed=${config.composed}`, + ); + const result = await getSequence(config.binding.sequence).run({ config, - programConfig, + input, boot, - options.composed ?? false, - ); + emit, + interaction: options.interaction, + signal, + }); + return { + ...result, + skillId: input.skillId, + snapshot: collector.snapshot(), + }; + } catch (error) { + // Not a decision the agent made. Hand it back whole rather than throw, so + // every ending of a run is a result the caller reads the same way. + const failure = classifyRunFailure(error); + logToFile('[agent-runner] run crashed:', error); + return { + outcome: 'crashed', + skillId: input.skillId, + failure: { + code: failure.code, + message: failure.message, + error: error instanceof Error ? error : new Error(String(error)), + }, + snapshot: collector.snapshot(), + }; } finally { - flushScanReport(session); + flushScanReport({ yaraReport: input.flags.yaraReport }); } } - -/** - * Resolve which sequence and harness will run a program (CLI → PostHog flag → - * per-program binding → default), tag both axes onto analytics, and return the - * binding for downstream dispatch. - * - * The one place `runner/index.ts` reaches into the switchboard — every other - * concern (bootstrap, cleanup, dispatch, per-task per-role harness picks) is - * either upstream or downstream of this call. - */ -function resolveProgramRunner( - session: WizardSession, - programConfig: ProgramConfig, - boot: BootstrapResult, - composed: boolean, -): ProgramBinding { - const ctx = { - program: programConfig.id, - composed, - flags: boot.wizardFlags, - flagPayloads: boot.wizardFlagPayloads, - cliHarness: session.harness, - cliSequence: session.sequence, - cliModel: session.model, - }; - const binding = resolveBinding(ctx); - tagBinding(boot, binding); - captureSwitchboardDecision(ctx, binding); - return binding; -} - -/** - * One event + one log line per run: what entered the switchboard, which - * precedence rung decided each axis, and the final pick. - */ -function captureSwitchboardDecision( - ctx: SwitchboardCtx, - binding: ProgramBinding, -): void { - const trace = ctx.trace ?? {}; - // Unpinned orchestrator runs choose a model per task from the context-mill agent prompts; the orchestrator logs that map once the prompts load. - const perTaskModel = - binding.sequence === Sequence.orchestrator && trace.model === 'binding'; - const model = perTaskModel ? 'chosen-per-task' : binding.model; - const modelSource = perTaskModel ? 'agent-prompts' : trace.model; - analytics.wizardCapture('switchboard resolved', { - program: ctx.program, - flag_self_driving_use_pi_harness: - ctx.flags[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY], - flag_self_driving_pi_payload: JSON.stringify( - ctx.flagPayloads?.[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY] ?? null, - ), - flag_orchestrator: ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY], - cli_harness: ctx.cliHarness, - cli_sequence: ctx.cliSequence, - cli_model: ctx.cliModel, - harness_source: trace.harness, - model_source: modelSource, - sequence_source: trace.sequence, - harness: binding.harness, - model, - thinking_level: binding.thinkingLevel, - sequence: binding.sequence, - }); - logToFile( - `[switchboard] decision: program=${ctx.program}` + - ` in(orchestrator=${ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY] ?? '-'},` + - ` cli=${ctx.cliHarness ?? '-'}/${ctx.cliSequence ?? '-'}/${ - ctx.cliModel ?? '-' - })` + - ` → harness=${binding.harness} (${trace.harness ?? '?'})` + - ` model=${model} (${modelSource ?? '?'})` + - ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, - ); -} - -/** - * Tag the run with its two routing axes. Sequence is stable for the whole - * run; harness reflects the run-level (default-role) resolution — orchestrator - * per-task calls emit their own `harness` property in their events so per-task - * aggregations attribute correctly. - */ -function tagBinding(boot: BootstrapResult, binding: ProgramBinding): void { - analytics.setTag('sequence', binding.sequence); - analytics.setTag('harness', binding.harness); - boot.wizardMetadata.SEQUENCE = binding.sequence; - boot.wizardMetadata.HARNESS = binding.harness; -} diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index 94ab86841..38aed52f1 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -1,112 +1,125 @@ /** * The linear pipeline. Single execution path for all non-orchestrator programs, * both skill-based (revenue analytics) and framework-based (core integration). - * The `ProgramRun` controls what varies between them; `programConfig` carries the - * program-level static metadata (tool allow/disallow lists, etc.). + * The `ProgramRun` controls what varies between them; `RunConfig` + * carries the program-level static metadata (tool allow/disallow lists, etc.). + * + * Reports through `emit`, asks through `interaction`, and returns a decided + * `RunResult`. Every former `getUI()` call is one progress event in the same + * place; every former `wizardAbort` is a returned failure with the same + * arguments, so the caller's exit sequence is unchanged. */ -import type { WizardSession } from '../../../wizard-session'; -import { OutroKind } from '../../../wizard-session'; -import { getUI } from '../../../../ui'; +import { OutroKind, type OutroData } from '@lib/wizard-session'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; -import { restoreClaudeSettings } from '../../claude-settings'; import { logToFile } from '../../../../utils/debug'; import { createBenchmarkPipeline } from '../../../middleware/benchmark'; -import { - wizardAbort, - WizardError, - registerCleanup, -} from '../../../../utils/wizard-abort'; +import { WizardError } from '../../../../utils/wizard-abort'; import { ErrorCodes, AGENT_ERROR_CODE } from '@lib/errors'; import { analytics } from '../../../../utils/analytics'; -import { - formatScanReport, - formatYaraAbortMessage, - writeScanReport, -} from '../../../yara-hooks'; +import { formatYaraAbortMessage } from '../../../yara-hooks'; import { installSkillById } from '../../../wizard-tools'; -import { createWizardAskBridge } from '../../../wizard-ask-bridge'; -import type { ProgramConfig } from '../../../programs/program-step'; import { assemblePrompt } from '../../agent-prompt'; -import type { ProgramRun, BootstrapResult } from '../shared/types'; -import { abortOnInstallFailure } from '../shared/errors'; -import { shouldDisableAsk, sessionToOptions } from '../shared/bootstrap'; -import { resolveHarness, getHarness } from '../switchboard'; +import type { + AgentFailure, + RunResult, + RunSnapshot, + SequenceContext, +} from '../shared/types'; +import { installFailure } from '../shared/errors'; +import { shouldDisableAsk, runOptions } from '../shared/bootstrap'; +import { createEmitSpinner } from '../shared/progress-collector'; +import { createAskBridge } from '../shared/ask'; +import { getHarness } from '../switchboard'; -export async function runLinearProgram( - session: WizardSession, - config: ProgramRun, - programConfig: ProgramConfig, - boot: BootstrapResult, - composed = false, -): Promise { - const { skillsBaseUrl, credentials, wizardFlags, project } = boot; +/** The snapshot the sequence returns; `runAgent` fills it from its collector. */ +const PENDING_SNAPSHOT: RunSnapshot = { + tasks: [], + statusMessages: [], + usage: { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + }, +}; + +const failed = (failure: AgentFailure): RunResult => ({ + outcome: 'failed', + failure, + snapshot: PENDING_SNAPSHOT, +}); + +const cancelled = (): RunResult => ({ + outcome: 'cancelled', + failure: { message: 'Wizard setup cancelled.' }, + snapshot: PENDING_SNAPSHOT, +}); + +export async function runLinearProgram({ + config, + input, + boot, + emit, + interaction, + signal, +}: SequenceContext): Promise { + const { run, composed } = config; + const { skillsBaseUrl, credentials, project } = boot; const { projectApiKey, host, projectId } = credentials; + if (signal.aborted) return cancelled(); + // 5. Skill install (if skillId provided) let skillPath: string | undefined; - if (config.skillId) { - logToFile(`[agent-runner] installing skill ${config.skillId}`); + if (run.skillId) { + logToFile(`[agent-runner] installing skill ${run.skillId}`); const installResult = await installSkillById( - config.skillId, - session.installDir, + run.skillId, + input.installDir, skillsBaseUrl, { triage: boot.triageProvider }, ); if (installResult.kind !== 'ok') { - await abortOnInstallFailure(config.integrationLabel, installResult); - return; + return failed(installFailure(run.integrationLabel, installResult)); } skillPath = installResult.path; logToFile(`[agent-runner] skill installed at ${skillPath}`); } - // 6. Initialize agent - const spinner = getUI().spinner(); - - const restoreSettings = () => restoreClaudeSettings(session.installDir); - getUI().onEnterScreen('outro', restoreSettings); + if (signal.aborted) return cancelled(); - if (session.yaraReport) { - registerCleanup(() => { - const reportPath = writeScanReport(); - if (reportPath) { - const summary = formatScanReport(); - getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ''}`); - } - }); - } + // 6. Initialize agent + const spinner = createEmitSpinner(emit); - getUI().startRun(); + emit({ kind: 'lifecycle', phase: 'started' }); // wizard_ask needs an answerer. A human answers at the keyboard; the e2e // snapshot/MCP host answers via its driver and sets WIZARD_ASK_AUTODRIVE. // CI/signup with neither has no answerer, so we omit the bridge and the tool // returns an actionable error rather than hanging on a never-resolving prompt. const askDisabled = - shouldDisableAsk(session) && process.env.WIZARD_ASK_AUTODRIVE !== '1'; - const askBridge = askDisabled + shouldDisableAsk(input.flags) && process.env.WIZARD_ASK_AUTODRIVE !== '1'; + const ask = askDisabled ? undefined - : createWizardAskBridge({ - getSource: () => session.skillId ?? config.integrationLabel, - showQuestion: (q) => getUI().requestQuestion(q), - cancelQuestion: () => getUI().cancelPendingQuestion(), - richLinks: config.richLinks ?? false, - timeoutMs: config.askTimeoutMs, + : createAskBridge(interaction, signal, { + getSource: () => input.skillId ?? run.integrationLabel, + richLinks: run.richLinks ?? false, + timeoutMs: run.askTimeoutMs, }); - const middleware = session.benchmark - ? createBenchmarkPipeline(spinner, sessionToOptions(session)) + const middleware = input.flags.benchmark + ? createBenchmarkPipeline(spinner, runOptions(input)) : undefined; // 7. Build prompt - const prompt = assemblePrompt(config, { + const prompt = assemblePrompt(run, { projectId, projectApiKey, host, skillPath, orgAiDataProcessingApproved: - session.apiUser?.organization?.is_ai_data_processing_approved ?? null, + input.apiUser?.organization?.is_ai_data_processing_approved ?? null, teamProductOptIns: project ? { sessionReplay: project.session_recording_opt_in ?? null, @@ -117,37 +130,34 @@ export async function runLinearProgram( }); logToFile(`[agent-runner] prompt assembled (${prompt.length} chars)`); - // 8. Resolve the (runner, model) pair from the central plan and run the agent - // through the selected runner. The runner owns the agent loop + model - // transport; everything around it (skill install, prompt, ask bridge, error - // routing, outro) stays here so every runner shares it. - const pick = resolveHarness({ - program: programConfig.id, - flags: wizardFlags, - flagPayloads: boot.wizardFlagPayloads, - cliHarness: session.harness, - cliModel: session.model, - }); - const agentResult = await getHarness(pick.harness).run({ - session, + // 8. Run the agent through the run-level harness. The harness owns the agent + // loop + model transport; everything around it (skill install, prompt, ask + // bridge, error routing, outro) stays here so every harness shares it. + const { harness, model, thinkingLevel } = config.binding; + const agentResult = await getHarness(harness).run({ config, - programConfig, + input, boot, + emit, prompt, skillPath, spinner, - askBridge, + askBridge: ask, middleware, - model: pick.model, - thinkingLevel: pick.thinkingLevel, + model, + thinkingLevel, }); - // 9. Error handling (full set from both runners) + // 9. Error handling (full set from both harnesses) + if (agentResult.failure) { + return failed(agentResult.failure); + } + if (agentResult.error === AgentErrorType.ABORT) { const reason = agentResult.message ?? ''; - const matched = config.abortCases?.find((c) => c.match.test(reason)); + const matched = run.abortCases?.find((c) => c.match.test(reason)); const abortCode = matched?.errorCode ?? ErrorCodes.AgentAbort; - const outroData: WizardSession['outroData'] = matched + const outroData: OutroData = matched ? { kind: OutroKind.Error, message: matched.message, @@ -158,44 +168,48 @@ export async function runLinearProgram( } : { kind: OutroKind.Error, - message: `${config.integrationLabel} aborted`, + message: `${run.integrationLabel} aborted`, body: reason || 'The agent aborted the program.', - docsUrl: config.docsUrl, + docsUrl: run.docsUrl, errorCode: abortCode, errorDetail: { reason }, }; analytics.wizardCapture('agent aborted', { - integration: config.integrationLabel, + integration: run.integrationLabel, reason, matched: matched?.message ?? null, }); - await wizardAbort({ - outroData, - code: abortCode, - error: new WizardError( - `Agent aborted: ${reason}`, - { - integration: config.integrationLabel, - error_type: AgentErrorType.ABORT, - reason, - }, - abortCode, - ), - }); + return { + outcome: 'aborted', + failure: { + outroData, + code: abortCode, + error: new WizardError( + `Agent aborted: ${reason}`, + { + integration: run.integrationLabel, + error_type: AgentErrorType.ABORT, + reason, + }, + abortCode, + ), + }, + snapshot: PENDING_SNAPSHOT, + }; } if (agentResult.error === AgentErrorType.MCP_MISSING) { - await wizardAbort({ + return failed({ code: AGENT_ERROR_CODE[AgentErrorType.MCP_MISSING], message: 'Could not access the PostHog MCP server\n\n' + 'The wizard was unable to connect to the PostHog MCP server.\n' + 'This could be due to a network issue or a configuration problem.\n\n' + - `Please try again, or check the documentation:\n${config.docsUrl}`, + `Please try again, or check the documentation:\n${run.docsUrl}`, error: new WizardError( 'Agent could not access PostHog MCP server', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.MCP_MISSING, signal: AgentSignals.ERROR_MCP_MISSING, }, @@ -205,16 +219,16 @@ export async function runLinearProgram( } if (agentResult.error === AgentErrorType.RESOURCE_MISSING) { - await wizardAbort({ + return failed({ code: AGENT_ERROR_CODE[AgentErrorType.RESOURCE_MISSING], message: 'Could not access the setup resource\n\n' + 'This may indicate a version mismatch or a temporary service issue.\n\n' + - `Please try again, or check the documentation:\n${config.docsUrl}`, + `Please try again, or check the documentation:\n${run.docsUrl}`, error: new WizardError( 'Agent could not access setup resource', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.RESOURCE_MISSING, signal: AgentSignals.ERROR_RESOURCE_MISSING, }, @@ -224,13 +238,13 @@ export async function runLinearProgram( } if (agentResult.error === AgentErrorType.YARA_VIOLATION) { - await wizardAbort({ + return failed({ code: AGENT_ERROR_CODE[AgentErrorType.YARA_VIOLATION], message: formatYaraAbortMessage(), error: new WizardError( 'YARA scanner terminated session', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.YARA_VIOLATION, }, AGENT_ERROR_CODE[AgentErrorType.YARA_VIOLATION], @@ -240,10 +254,10 @@ export async function runLinearProgram( if (agentResult.error === AgentErrorType.NO_PROGRESS) { analytics.wizardCapture('agent no progress', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.NO_PROGRESS, }); - await wizardAbort({ + return failed({ code: AGENT_ERROR_CODE[AgentErrorType.NO_PROGRESS], message: 'The Wizard exited without changing your project. Please contact the ' + @@ -251,7 +265,7 @@ export async function runLinearProgram( error: new WizardError( 'Agent made no progress', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.NO_PROGRESS, }, AGENT_ERROR_CODE[AgentErrorType.NO_PROGRESS], @@ -261,10 +275,10 @@ export async function runLinearProgram( if (agentResult.error === AgentErrorType.INCOMPLETE_TASKS) { analytics.wizardCapture('agent incomplete tasks', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.INCOMPLETE_TASKS, }); - await wizardAbort({ + return failed({ code: AGENT_ERROR_CODE[AgentErrorType.INCOMPLETE_TASKS], message: 'The Wizard exited without completing its planned tasks. Please contact ' + @@ -272,7 +286,7 @@ export async function runLinearProgram( error: new WizardError( 'Agent left planned tasks incomplete', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: AgentErrorType.INCOMPLETE_TASKS, }, AGENT_ERROR_CODE[AgentErrorType.INCOMPLETE_TASKS], @@ -285,12 +299,12 @@ export async function runLinearProgram( agentResult.error === AgentErrorType.API_ERROR ) { analytics.wizardCapture('agent api error', { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: agentResult.error, error_message: agentResult.message, }); - await wizardAbort({ + return failed({ code: AGENT_ERROR_CODE[agentResult.error], message: `API Error\n\n${ agentResult.message || 'Unknown error' @@ -298,7 +312,7 @@ export async function runLinearProgram( error: new WizardError( `API error: ${agentResult.message}`, { - integration: config.integrationLabel, + integration: run.integrationLabel, error_type: agentResult.error, }, AGENT_ERROR_CODE[agentResult.error], @@ -307,39 +321,36 @@ export async function runLinearProgram( } // 10. Post-run hooks - if (config.postRun) { - await config.postRun(session, credentials); + if (config.hooks?.postRun) { + await config.hooks.postRun(credentials); } // A composed sub-run (integration inside self-driving) skips the terminal // outro + analytics shutdown so the shared client survives the host's run. - if (composed) return; + if (composed) { + return { outcome: 'success', snapshot: PENDING_SNAPSHOT }; + } // 11. Outro - // Push outro data through the UI (not via direct `session.outroData = ...` - // mutation) so the live store gets the value. agent-runner's `session` - // parameter is captured at runAgent() invocation time, and any `setKey` - // call between then and here (e.g. setDashboardUrl, setNotebookUrl) forks - // the session reference — direct mutation then lands on a stale snapshot - // that the screen never reads. UI.setOutroData() goes through the store - // and also merges in any post-snapshot URLs from the live session. - const outroData = config.buildOutroData - ? config.buildOutroData(session, credentials) + const outroData: OutroData | undefined = config.hooks?.buildOutroData + ? config.hooks.buildOutroData(credentials) : { kind: OutroKind.Success, - message: config.successMessage, - reportFile: config.reportFile, - docsUrl: config.docsUrl, - continueUrl: session.signup + message: run.successMessage, + reportFile: run.reportFile, + docsUrl: run.docsUrl, + continueUrl: input.flags.signup ? `${host.appHost}/products?source=wizard` : undefined, }; if (outroData) { - getUI().setOutroData(outroData); + emit({ kind: 'completion', outro: outroData }); } - getUI().outro(config.successMessage); + emit({ kind: 'lifecycle', phase: 'completed', message: run.successMessage }); // 12. Analytics shutdown await analytics.shutdown('success'); + + return { outcome: 'success', outro: outroData, snapshot: PENDING_SNAPSHOT }; } diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts index d2379b32c..9ee3fca2a 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts @@ -10,9 +10,6 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -vi.mock('@ui', () => ({ - getUI: () => ({ showTaskNotice: vi.fn(), cancelTaskNotice: vi.fn() }), -})); vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn(), diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts index c6acf1014..40c6642f8 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts @@ -19,9 +19,6 @@ const { showTaskNotice, cancelTaskNotice, wizardCapture, captureException } = captureException: vi.fn(), })); -vi.mock('@ui', () => ({ - getUI: () => ({ showTaskNotice, cancelTaskNotice }), -})); vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture, @@ -38,6 +35,12 @@ import { TASK_NOTICE_TIMEOUT_MS, } from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +/** The answerer under test, standing where `getUI()` used to. */ +const interaction = { + taskNotice: (notice: TaskNotice) => showTaskNotice(notice), + cancelTaskNotice: () => cancelTaskNotice(), +}; + const NOTICE: TaskNotice = { title: 'Connect your data sources', body: ['We found some sources.'], @@ -67,7 +70,7 @@ describe('task notice timeout', () => { // Nobody presses anything. showTaskNotice.mockReturnValue(new Promise(() => undefined)); - const promise = offerSeededTask(NOTICE, 1000); + const promise = offerSeededTask(NOTICE, 1000, interaction); vi.advanceTimersByTime(1000); await expect(promise).resolves.toEqual({ keep: false, timedOut: true }); @@ -83,7 +86,7 @@ describe('task notice timeout', () => { try { showTaskNotice.mockResolvedValue(true); - const result = await offerSeededTask(NOTICE, 1000); + const result = await offerSeededTask(NOTICE, 1000, interaction); expect(result).toEqual({ keep: true, timedOut: false }); vi.advanceTimersByTime(5000); @@ -102,10 +105,12 @@ describe('task notice timeout', () => { // Both decline, but only one of them means "the user was not there" — // the run reports them differently, and now skips them differently too. - await expect(offerSeededTask(NOTICE, 1000)).resolves.toEqual({ - keep: false, - timedOut: false, - }); + await expect(offerSeededTask(NOTICE, 1000, interaction)).resolves.toEqual( + { + keep: false, + timedOut: false, + }, + ); expect(cancelTaskNotice).not.toHaveBeenCalled(); } finally { vi.useRealTimers(); @@ -164,7 +169,9 @@ describe('askSeededConsent', () => { it('records an acceptance', async () => { showTaskNotice.mockResolvedValue(true); - await expect(askSeededConsent('warehouse', NOTICE, 1000)).resolves.toEqual({ + await expect( + askSeededConsent('warehouse', NOTICE, 1000, interaction), + ).resolves.toEqual({ keep: true, timedOut: false, errored: false, @@ -174,7 +181,9 @@ describe('askSeededConsent', () => { it('records an explicit decline', async () => { showTaskNotice.mockResolvedValue(false); - await expect(askSeededConsent('warehouse', NOTICE, 1000)).resolves.toEqual({ + await expect( + askSeededConsent('warehouse', NOTICE, 1000, interaction), + ).resolves.toEqual({ keep: false, timedOut: false, errored: false, @@ -186,7 +195,7 @@ describe('askSeededConsent', () => { try { showTaskNotice.mockReturnValue(new Promise(() => undefined)); - const promise = askSeededConsent('warehouse', NOTICE, 1000); + const promise = askSeededConsent('warehouse', NOTICE, 1000, interaction); vi.advanceTimersByTime(1000); await expect(promise).resolves.toEqual({ @@ -202,7 +211,7 @@ describe('askSeededConsent', () => { it('reports the answer on one event per offer', async () => { showTaskNotice.mockResolvedValue(true); - await askSeededConsent('warehouse', NOTICE, 1000); + await askSeededConsent('warehouse', NOTICE, 1000, interaction); expect(wizardCapture).toHaveBeenCalledTimes(1); expect(wizardCapture).toHaveBeenCalledWith( @@ -216,7 +225,9 @@ describe('askSeededConsent', () => { // that could not be put to the user must never be read as a yes. showTaskNotice.mockRejectedValue(new Error('UI blew up')); - await expect(askSeededConsent('warehouse', NOTICE, 1000)).resolves.toEqual({ + await expect( + askSeededConsent('warehouse', NOTICE, 1000, interaction), + ).resolves.toEqual({ keep: false, timedOut: false, errored: true, @@ -247,7 +258,9 @@ describe('offering notices from the seed loop', () => { const answers: { keep: boolean; timedOut: boolean; errored: boolean }[] = []; for (const entry of entries) { - answers.push(await askSeededConsent(entry.type, NOTICE, 60_000)); + answers.push( + await askSeededConsent(entry.type, NOTICE, 60_000, interaction), + ); } return answers; }; diff --git a/src/lib/agent/runner/sequence/orchestrator/executor.ts b/src/lib/agent/runner/sequence/orchestrator/executor.ts index 616d29442..e17e8c269 100644 --- a/src/lib/agent/runner/sequence/orchestrator/executor.ts +++ b/src/lib/agent/runner/sequence/orchestrator/executor.ts @@ -10,6 +10,7 @@ * injected: the real one spins up a fresh agent, the tests use a fake. */ import { analytics } from '@utils/analytics'; +import type { AgentFailure } from '../../shared/types'; import { logToFile } from '@utils/debug'; import { TaskStatus, type QueueStore, type QueuedTask } from './queue'; @@ -33,6 +34,19 @@ export type TaskResolver = ( * (via the task agent calling complete_task). */ export type RunTask = (task: QueuedTask) => Promise; +/** + * Thrown by a `RunTask` when its harness returned a decided failure that ends + * the whole run (a 401 the auth screen already reported), not just the task. + * `drainQueue` lets it through so the sequence can return the failure where + * the harness used to exit the process. + */ +export class RunTaskFatal extends Error { + constructor(public readonly failure: AgentFailure) { + super(failure.message ?? 'agent run failed'); + this.name = 'RunTaskFatal'; + } +} + export interface DrainOptions { /** Backstop against a pathological always-one-more-pending loop. */ maxStarts: number; @@ -51,6 +65,7 @@ async function runOne( try { await runTask(task); } catch (error) { + if (error instanceof RunTaskFatal) throw error; // The task threw rather than reporting. The outcome check below handles // the queue; the exception itself should never be silent. logToFile(`[executor] runTask threw for ${task.type}:`, error); diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 8778431ad..da9b0d4e2 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -20,31 +20,28 @@ import { writeFileSync, } from 'fs'; import * as path from 'path'; -import { - OutroKind, - type TaskNotice, - type WizardSession, -} from '@lib/wizard-session'; -import { - POSTHOG_DOCS_URL, - WIZARD_CONTACT_EMAIL, - type Integration, -} from '@lib/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { OutroKind, type TaskNotice } from '@lib/wizard-session'; +import { POSTHOG_DOCS_URL, WIZARD_CONTACT_EMAIL } from '@lib/constants'; import { installSkillById, fetchSkillMenu, type SkillEntry, } from '@lib/wizard-tools'; -import { getUI } from '@ui'; import { analytics } from '@utils/analytics'; import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; -import { wizardAbort, WizardError } from '@utils/wizard-abort'; +import { WizardError } from '@utils/wizard-abort'; import { ErrorCodes } from '@lib/errors'; -import type { ProgramConfig } from '@lib/programs/program-step'; -import type { BootstrapResult, ProgramRun } from '../../shared/types'; +import type { AgentInteraction } from '@lib/agent/progress'; +import type { + AgentFailure, + RunResult, + RunSnapshot, + SequenceContext, +} from '../../shared/types'; +import { createEmitSpinner } from '../../shared/progress-collector'; +import { createAskBridge } from '../../shared/ask'; import { areSeededTasksEnabled, getHarness, @@ -61,14 +58,11 @@ import { TaskStatus, type QueuedTask, } from './queue'; -import { drainQueue, type RunTask } from './executor'; +import { drainQueue, RunTaskFatal, type RunTask } from './executor'; import { RunMetrics } from './run-metrics'; import { dependencyClosure, uncoveredBySink } from './queue-tools'; import { deferSeededTasks } from './seeded-deps'; -import { - createWizardAskBridge, - LONGER_ASK_TIMEOUT_MS, -} from '@lib/wizard-ask-bridge'; +import { LONGER_ASK_TIMEOUT_MS } from '@lib/wizard-ask-bridge'; import { shouldDisableAsk } from '../../shared/bootstrap'; import { agentRunTools, @@ -183,7 +177,7 @@ export function resolveSkillVariantId( } /** - * The framework reference is the full `integration` skill. `session.skillId` is + * The framework reference is the full `integration` skill. `input.skillId` is * the bare framework (e.g. `django`), but the skill menu ids it as * `integration-`. */ @@ -222,7 +216,14 @@ export const TASK_NOTICE_TIMEOUT_MS = 5 * 60 * 1000; export async function offerSeededTask( notice: TaskNotice, timeoutMs: number = TASK_NOTICE_TIMEOUT_MS, + interaction?: AgentInteraction, + signal: AbortSignal = new AbortController().signal, ): Promise<{ keep: boolean; timedOut: boolean }> { + // No one to show the notice to: a step nobody can answer for must not run. + // The same answer a non-interactive host gives today. + if (!interaction?.taskNotice) return { keep: false, timedOut: false }; + const { taskNotice, cancelTaskNotice } = interaction; + let timer: ReturnType | undefined; let timedOut = false; const timeout = new Promise((resolve) => { @@ -230,12 +231,12 @@ export async function offerSeededTask( timedOut = true; // Dismisses the overlay and settles the showTaskNotice promise too, so // the losing side of the race cannot leave a modal on screen. - getUI().cancelTaskNotice(); + cancelTaskNotice?.(); resolve(false); }, timeoutMs); }); try { - const keep = await Promise.race([getUI().showTaskNotice(notice), timeout]); + const keep = await Promise.race([taskNotice(notice, { signal }), timeout]); return { keep, timedOut }; } finally { if (timer) clearTimeout(timer); @@ -337,8 +338,15 @@ export async function askSeededConsent( type: string, notice: TaskNotice, timeoutMs?: number, + interaction?: AgentInteraction, + signal?: AbortSignal, ): Promise { - const consent = await offerSeededTask(notice, timeoutMs).then( + const consent = await offerSeededTask( + notice, + timeoutMs, + interaction, + signal, + ).then( (answer): SeededConsent => ({ ...answer, errored: false }), (err: unknown): SeededConsent => { logToFile( @@ -427,33 +435,58 @@ export function displayOrder( .map((entry) => entry.task); } -export async function runOrchestrator( - session: WizardSession, - config: ProgramRun, - programConfig: ProgramConfig, - boot: BootstrapResult, -): Promise { +/** The snapshot the sequence returns; `runAgent` fills it from its collector. */ +const PENDING_SNAPSHOT: RunSnapshot = { + tasks: [], + statusMessages: [], + usage: { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + }, +}; + +const failed = (failure: AgentFailure): RunResult => ({ + outcome: 'failed', + failure, + snapshot: PENDING_SNAPSHOT, +}); + +export async function runOrchestrator({ + config, + input, + boot, + emit, + interaction, + signal, +}: SequenceContext): Promise { const runId = randomUUID(); + const { run } = config; + const programId = config.programId; + + if (signal.aborted) { + return { + outcome: 'cancelled', + failure: { message: 'Wizard setup cancelled.' }, + snapshot: PENDING_SNAPSHOT, + }; + } // Switchboard context — reused for every per-role harness resolution below. - const switchboardCtx = { - program: programConfig.id, - flags: boot.wizardFlags, - flagPayloads: boot.wizardFlagPayloads, - cliHarness: session.harness, - cliSequence: session.sequence, - cliModel: session.model, - }; + // The caller resolved the run-level binding from it; per-task roles overlay + // `binding.contextMillOverride[role]` on the same inputs. + const switchboardCtx = { ...config.switchboard, trace: undefined }; // The WHAT (agent prompts) is served from context-mill. Fetch the registry // once up front: its types drive enqueue validation, and resolving a task to // its run config is then synchronous, with no mid-drain network latency. - const flow = programConfig.agentFlow ?? programConfig.id; + const flow = config.agentFlow ?? programId; const registry = await loadAgentRegistry(boot.skillsBaseUrl, flow, { exclude: ciExcludedTaskTypes(), // Baked into the prompts at load, so enqueue, dispatch, and telemetry all read one effective spec. overrides: resolveStageOverrides( - programConfig.id, + programId, boot.wizardFlags, boot.wizardFlagPayloads, ), @@ -492,7 +525,7 @@ export async function runOrchestrator( ? Date.parse(t.finishedAt) - Date.parse(t.startedAt) : undefined; - const store = new QueueStore(session.installDir, runId, { + const store = new QueueStore(input.installDir, runId, { onTransition: (event, task) => { const pick = resolveHarness(switchboardCtx, task.type); // Mirror dispatch's allow-list fallback so attribution names the model that runs. @@ -565,20 +598,20 @@ export async function runOrchestrator( let commandmentsPath: string | undefined; let referenceInstallPath: string | undefined; const menuSkillEntries = await fetchSkillMenuEntries(boot.skillsBaseUrl); - // The framework key for reference + variant resolution. `session.integration` - // is the detected framework and always wins; `session.skillId` is the - // fallback for the basic-integration path, where bootstrap sets it to the + // The framework key for reference + variant resolution. `input.integration` + // is the detected framework and always wins; `input.skillId` is the + // fallback for the basic-integration path, where the caller sets it to the // framework label. Programs whose run config carries their own skill id // (agent-skill commands like replay-vision) would otherwise leak that id in - // here as a bogus framework after bootstrap overwrites the detect result. - const framework = session.integration ?? session.skillId ?? undefined; + // here as a bogus framework after the caller overwrites the detect result. + const framework = input.integration ?? input.skillId ?? undefined; const referenceSkillId = framework ? resolveReferenceSkillId(menuSkillEntries, framework) : undefined; if (referenceSkillId) { const ref = await installSkillById( referenceSkillId, - session.installDir, + input.installDir, boot.skillsBaseUrl, { skillsRoot: path.join(QUEUE_DIR_NAME, 'reference'), @@ -588,11 +621,11 @@ export async function runOrchestrator( if (ref.kind === 'ok') { referenceInstallPath = ref.path; const example = path.join(ref.path, 'references', 'EXAMPLE.md'); - if (existsSync(path.join(session.installDir, example))) { + if (existsSync(path.join(input.installDir, example))) { examplePath = example; } const commandments = path.join(ref.path, 'references', 'COMMANDMENTS.md'); - if (existsSync(path.join(session.installDir, commandments))) { + if (existsSync(path.join(input.installDir, commandments))) { commandmentsPath = commandments; } } else { @@ -627,11 +660,9 @@ export async function runOrchestrator( } } if (missingVariants.length > 0) { - // The framework's own docs page from its config; generic docs when detection found none. - const docsUrl = framework - ? FRAMEWORK_REGISTRY[framework as Integration]?.metadata.docsUrl - : undefined; - await wizardAbort({ + // The framework's own docs page, resolved by the caller; generic docs when detection found none. + const docsUrl = framework ? input.frameworkDocsUrl : undefined; + return failed({ code: ErrorCodes.AgentOrchestratorSkillVariantMissing, message: 'Setup instructions for this project failed to download.\n' + @@ -662,27 +693,28 @@ export async function runOrchestrator( }; logToFile( - `[orchestrator] START program=${programConfig.id} dir=${session.installDir} run=${runId}`, + `[orchestrator] START program=${programId} dir=${input.installDir} run=${runId}`, ); analytics.wizardCapture('orchestrator started', { - program_id: programConfig.id, + program_id: programId, }); - getUI().startRun(); + emit({ kind: 'lifecycle', phase: 'started' }); // Label precedence: what the orchestrator set at enqueue, then the agent // prompt's default, then the bare type. const labelFor = (t: { type: string; label?: string }) => t.label ?? registry.get(t.type)?.label ?? t.type; const renderQueue = () => - getUI().syncTodos( - displayOrder(store.list(), (t) => + emit({ + kind: 'tasks', + tasks: displayOrder(store.list(), (t) => registry.runnerSeededTypes.includes(t.type), ).map((t) => ({ content: labelFor(t), status: toTodoStatus(t.status), activeForm: labelFor(t), })), - ); + }); // Each task's run binds the wizard-tools MCP server to a per-task // orchestrator context so complete_task / enqueue_task attribute correctly @@ -709,7 +741,7 @@ export async function runOrchestrator( // Kill switch: off (or unset), the wizard queues nothing itself and the run // is byte-identical to a project with no detected sources. const seedEntries = areSeededTasksEnabled(boot.wizardFlags) - ? programConfig.seedTasks?.(session) ?? [] + ? config.seedTasks?.() ?? [] : []; const seededTypes: string[] = []; // Kept so their dependencies can be resolved once the planner has run — they @@ -758,7 +790,13 @@ export async function runOrchestrator( // not, which is why the offer lives here and not there. seededConsent.set( task.id, - await askSeededConsent(seeded.type, seeded.notice), + await askSeededConsent( + seeded.type, + seeded.notice, + undefined, + interaction, + signal, + ), ); } logToFile(`[orchestrator] runner-seeded task ${seeded.type}`); @@ -790,11 +828,11 @@ export async function runOrchestrator( // One bridge for the run, handed only to a task whose prompt allows asking. // Absent in CI and signup, where nobody can answer. - const askBridge = shouldDisableAsk(session) + const askBridge = shouldDisableAsk(input.flags) ? undefined - : createWizardAskBridge({ - getSource: () => session.skillId ?? programConfig.id, - showQuestion: (q) => { + : createAskBridge(interaction, signal, { + getSource: () => input.skillId ?? programId, + beforeShow: () => { // How late the first ask lands is the measure of this run shape: it // should follow the autonomous work, not interrupt it. metrics.recordAsk(Date.now()); @@ -804,16 +842,14 @@ export async function runOrchestrator( // unanswered ask still times out into the deep-link fallback — it just // gives a person who stepped away a chance to come back first. ringTerminalBell(); - return getUI().requestQuestion(q); }, - cancelQuestion: () => getUI().cancelPendingQuestion(), - richLinks: config.richLinks ?? false, + richLinks: run.richLinks ?? false, // A task ask waits on a person, and the drain waits it out — the // executor holds the task's promise — so this is the only real limit. timeoutMs: LONGER_ASK_TIMEOUT_MS, }); - const spinner = getUI().spinner(); + const spinner = createEmitSpinner(emit); // 1. Seed the queue with the orchestrator agent. It is itself an agent prompt // (the WHAT), so its model and tools come from its frontmatter. The seed @@ -826,9 +862,10 @@ export async function runOrchestrator( const seedHarness = requireTaskHarness(seedPick); const seedModel = promptModelFor(seedPrompt, seedPick.harness); const seedResult = await seedHarness.runTask({ - session, - programConfig, + config, + input, boot, + emit, prompt: assembleSeedPrompt(promptContext, seedPrompt.body, store.list()), spinner, model: requireKnownModel(seedModel.model, seedPick.model), @@ -841,6 +878,9 @@ export async function runOrchestrator( requestRemark: false, analyticsProperties: { task_type: 'seed', harness: seedPick.harness }, }); + // A decided failure (a 401 the harness already reported) ends the run here, + // before anything is queued, exactly where the harness used to exit. + if (seedResult.failure) return failed(seedResult.failure); if (seedResult.error) { logToFile( `[orchestrator] seed error: ${seedResult.error} ${ @@ -941,7 +981,7 @@ export async function runOrchestrator( analytics.wizardCapture('orchestrator sink invariant violated', { uncovered_types: unwaited.map((t) => t.type), }); - await wizardAbort({ + return failed({ code: ErrorCodes.AgentOrchestratorSinkInvariant, message: `The wizard could not plan this setup: the final step would have skipped ${unwaited .map((t) => t.type) @@ -965,7 +1005,7 @@ export async function runOrchestrator( // Task agents can install durable skills mid-run (load_skill), and only the // framework reference docs earn a place — snapshot what was already there so // the sweeps remove exactly what this run added. - const claudeSkillsDir = path.join(session.installDir, '.claude', 'skills'); + const claudeSkillsDir = path.join(input.installDir, '.claude', 'skills'); const preexistingSkills = new Set( existsSync(claudeSkillsDir) ? readdirSync(claudeSkillsDir) : [], ); @@ -998,7 +1038,7 @@ export async function runOrchestrator( } const result = await installSkillById( variantId, - session.installDir, + input.installDir, boot.skillsBaseUrl, { skillsRoot: taskSkillsRoot, triage: boot.triageProvider }, ); @@ -1029,10 +1069,11 @@ export async function runOrchestrator( const taskPick = resolveHarness(switchboardCtx, task.type); const taskHarness = requireTaskHarness(taskPick); const taskModel = taskModelSpec(registry, task, taskPick.harness); - await taskHarness.runTask({ - session, - programConfig, + const taskResult = await taskHarness.runTask({ + config, + input, boot, + emit, prompt: assembleTaskPrompt(promptContext, resolved.prompt, skillPaths), spinner, model: requireKnownModel(taskModel.model, taskPick.model), @@ -1051,6 +1092,10 @@ export async function runOrchestrator( harness: taskPick.harness, }, }); + // A decided failure (a 401 the harness already reported) is the run's, + // not the task's: stop the drain and report it, where the harness used + // to exit the process. + if (taskResult.failure) throw new RunTaskFatal(taskResult.failure); } finally { // Durable skills a task installed are irrelevant to later tasks — and // the sdk harness auto-loads .claude/skills into every agent — so sweep @@ -1074,13 +1119,17 @@ export async function runOrchestrator( renderQueue(); } + let fatal: AgentFailure | undefined; try { await drainQueue(store, runTask); + } catch (error) { + if (!(error instanceof RunTaskFatal)) throw error; + fatal = error.failure; } finally { try { if (referenceSkillId && referenceInstallPath) { promoteReferenceSkill( - path.join(session.installDir, referenceInstallPath), + path.join(input.installDir, referenceInstallPath), claudeSkillsDir, referenceSkillId, ); @@ -1095,7 +1144,7 @@ export async function runOrchestrator( // cache folder (queue, handoffs, reference example, installed task // instructions). The .DELETE-ME.md inside is the fallback if we don't. try { - rmSync(path.join(session.installDir, QUEUE_DIR_NAME), { + rmSync(path.join(input.installDir, QUEUE_DIR_NAME), { recursive: true, force: true, }); @@ -1119,6 +1168,8 @@ export async function runOrchestrator( } } + if (fatal) return failed(fatal); + renderQueue(); const summary = store.summary(); @@ -1172,7 +1223,7 @@ export async function runOrchestrator( ', ', )}.\n\nPlease try again, approving all permissions on the PostHog authorization screen. If it still fails, report it to: ${WIZARD_CONTACT_EMAIL}` : `The wizard was unable to set up PostHog: ${whatFailed}.\n\nPlease report this to: ${WIZARD_CONTACT_EMAIL}`; - await wizardAbort({ + return failed({ code: ErrorCodes.AgentOrchestratorTasksFailed, message, error: new WizardError( @@ -1193,7 +1244,7 @@ export async function runOrchestrator( // usable model response (e.g. the gateway returned empty completions). // "0/0 completed" is a dead run, not a success with an empty denominator. if (summary.total === 0) { - await wizardAbort({ + return failed({ code: ErrorCodes.AgentOrchestratorHollowRun, message: `The wizard was unable to set up PostHog: the planning step produced no work, so nothing ran.\n\nPlease try again — and if it happens again, report it to: ${WIZARD_CONTACT_EMAIL}`, error: new WizardError( @@ -1219,19 +1270,20 @@ export async function runOrchestrator( } steps completed${ stepNotes.length > 0 ? ` (${stepNotes.join(', ')})` : '' }.`; - getUI().setOutroData({ + const outro = { kind: OutroKind.Success, message, body: conflict ? `⚠ Build conflict: ${conflict}\nFull details are in the setup report.` : undefined, docsUrl: 'https://posthog.com/docs/ai-engineering/ai-wizard', - nextSteps: config.buildOutroNextSteps?.( - session, + nextSteps: config.hooks?.buildOutroNextSteps?.( boot.credentials, completedSeededTypes(store, seededTasks), ), - }); - getUI().outro(message); + }; + emit({ kind: 'completion', outro }); + emit({ kind: 'lifecycle', phase: 'completed', message }); await analytics.shutdown('success'); + return { outcome: 'success', outro, snapshot: PENDING_SNAPSHOT }; } diff --git a/src/lib/agent/runner/shared/ask.ts b/src/lib/agent/runner/shared/ask.ts new file mode 100644 index 000000000..b109f053b --- /dev/null +++ b/src/lib/agent/runner/shared/ask.ts @@ -0,0 +1,42 @@ +/** + * The ask bridge over an injected answerer. + * + * `createWizardAskBridge` already owns request ids, the timeout race, the + * `__cancelled__` sentinel and the analytics; it only ever needed a + * `showQuestion` and a `cancelQuestion`. Here those come from + * `AgentInteraction` instead of `getUI()`. With no answerer there is no bridge, + * so `wizard_ask` reports its existing "not available" error rather than + * hanging on a question nobody can see. + */ + +import { + createWizardAskBridge, + type WizardAskBridge, +} from '@lib/wizard-ask-bridge'; +import type { AgentInteraction } from '@lib/agent/progress'; + +export function createAskBridge( + interaction: AgentInteraction | undefined, + signal: AbortSignal, + options: { + getSource: () => string; + richLinks: boolean; + timeoutMs?: number; + /** Runs before each question is shown (the orchestrator's bell and metric). */ + beforeShow?: () => void; + }, +): WizardAskBridge | undefined { + const ask = interaction?.ask; + if (!ask) return undefined; + + return createWizardAskBridge({ + getSource: options.getSource, + showQuestion: (question) => { + options.beforeShow?.(); + return ask(question, { signal }); + }, + cancelQuestion: interaction?.cancelAsk, + richLinks: options.richLinks, + timeoutMs: options.timeoutMs, + }); +} diff --git a/src/lib/agent/runner/shared/bootstrap.ts b/src/lib/agent/runner/shared/bootstrap.ts index dc12a2b60..1b776a87c 100644 --- a/src/lib/agent/runner/shared/bootstrap.ts +++ b/src/lib/agent/runner/shared/bootstrap.ts @@ -1,46 +1,22 @@ /** - * Shared bootstrap for the runner pipeline. + * Shared preparation for the runner pipeline. * - * Runs before the fork into the linear or orchestrator arm: logging, health - * check, settings conflicts, OAuth and credentials, feature flags, variant - * metadata, and MCP url. Sets `session.credentials`, role, and user as side - * effects. Returns the values the arms still need. + * Runs before the fork into the linear or orchestrator arm: logging targets, + * the gateway mint and the scan-triage classifier built on it. Everything the + * caller must decide first — health gates, settings conflicts, authentication, + * the AI opt-in gate, post-auth gates, feature flags, run tags, token refresh — + * arrives already resolved in `RunConfig` and `RunInput`. */ -import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; -import { getUI } from '@ui'; -import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; -import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; import { createTriageLLMProvider } from '@lib/agent/triage-provider'; import { gatewayAuth } from '@lib/gateway-session'; -import { resolveHarness } from '../switchboard'; -import { buildRunTags } from '@lib/agent/agent-interface'; -import { - checkAllSettingsConflicts, - backupAndFixClaudeSettings, - classifySettingsConflicts, -} from '@lib/agent/claude-settings'; -import { - evaluateWizardReadiness, - WizardReadiness, - SIGNUP_WIZARD_READINESS_CONFIG, - getBlockingServiceKeys, - SERVICE_LABELS, -} from '@lib/health-checks/readiness'; import { enableDebugLogs, logToFile, initLogFile } from '@utils/debug'; -import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; -import { isNonInteractiveEnvironment } from '@utils/environment'; -import { CallType, getSkillsBaseUrl, IS_DEV } from '@lib/constants'; +import { CallType, IS_DEV } from '@lib/constants'; import { VERSION } from '@lib/version'; import { mcpUrlFor } from '@lib/host-resolution'; import type { WizardRunOptions } from '@utils/types'; -import { - postAuthGateSteps, - type ProgramConfig, -} from '@lib/programs/program-step'; -import type { ProgramRun, BootstrapResult } from './types'; +import type { BootstrapResult, RunConfig, RunFlags, RunInput } from './types'; // ── Helpers ────────────────────────────────────────────────────────── @@ -51,7 +27,7 @@ import type { ProgramRun, BootstrapResult } from './types'; * 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` + * `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 @@ -61,254 +37,68 @@ import type { ProgramRun, BootstrapResult } from './types'; * populates it, so plain `--ci` and `--signup` runs behave exactly as before. */ export function shouldDisableAsk( - session: Pick, + flags: Pick, ): boolean { - return (session.ci || session.signup) && !session.e2eAsk; + return (flags.ci || flags.signup) && !flags.e2eAsk; } -export function sessionToOptions(session: WizardSession): WizardRunOptions { +/** The option bag the agent interface and the middleware read. */ +export function runOptions(input: RunInput): WizardRunOptions { return { - installDir: session.installDir, - debug: session.debug, - signup: session.signup, - ci: session.ci, - benchmark: session.benchmark, - projectId: session.projectId, - apiKey: session.apiKey, - yaraReport: session.yaraReport, + installDir: input.installDir, + debug: input.flags.debug, + signup: input.flags.signup, + ci: input.flags.ci, + benchmark: input.flags.benchmark, + projectId: input.host.projectId, + apiKey: input.host.apiKey, + yaraReport: input.flags.yaraReport, }; } -// ── Bootstrap ───────────────────────────────────────────────────────── +// ── Prepare ─────────────────────────────────────────────────────────── /** - * Shared setup for both arms: logging, health check, settings conflicts, OAuth - * and credentials, then the feature flags, variant metadata, and MCP url. Sets - * `session.credentials`, role, and user as a side effect. Returns the values the - * arms still need. + * Shared setup for both arms: logging targets, then the gateway mint and the + * triage classifier. Throws when the mint is refused, so the run fails before + * any agent starts — the caller maps that the way it maps any unexpected error. */ -export async function bootstrapProgram( - session: WizardSession, - config: ProgramRun, - programConfig: ProgramConfig, +export async function prepareRun( + config: RunConfig, + input: RunInput, ): Promise { + const { run } = config; + // 1. Init logging + debug initLogFile(); - session.skillId = config.skillId ?? config.integrationLabel; logToFile( - `[agent-runner] START ${config.integrationLabel} build=${analytics.build}` + - `${session.ci ? ' (non-interactive)' : ''}`, + `[agent-runner] START ${run.integrationLabel} build=${analytics.build}` + + `${input.flags.ci ? ' (non-interactive)' : ''}`, ); - if (session.debug) { + if (input.flags.debug) { enableDebugLogs(); } - const skillsBaseUrl = getSkillsBaseUrl(); + const { skillsBaseUrl } = config; // Where this run actually points. The three services switch independently, // so otherwise "why did it use prod skills?" means reading three call sites. logToFile( `[agent-runner] targets build=${VERSION}${IS_DEV ? '/dev' : ''} ` + `skills=${skillsBaseUrl} ` + - `mcp=${mcpUrlFor(session.localMcp)} ` + - `posthog=${session.baseUrl ?? 'region-resolved'}`, - ); - - // 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', - ); - if (session.readinessResult) { - logToFile( - `[agent-runner] readiness pre-computed by TUI: decision=${session.readinessResult.decision}` + - `${ - session.outageDismissed ? ' (outage dismissed by user)' : '' - } — skipping re-check`, - ); - } - if (hasHealthCheckScreen && !session.readinessResult) { - logToFile('[agent-runner] evaluating wizard readiness'); - const readinessConfig = session.signup - ? SIGNUP_WIZARD_READINESS_CONFIG - : undefined; - const readiness = await evaluateWizardReadiness(readinessConfig); - logToFile(`[agent-runner] readiness=${readiness.decision}`); - if (readiness.decision === WizardReadiness.No) { - const blockingKeys = getBlockingServiceKeys( - readiness.health, - readinessConfig, - ); - const blockingLabels = blockingKeys.map( - (k) => `${SERVICE_LABELS[k]} (${readiness.health[k].status})`, - ); - logToFile(`[agent-runner] blocked by: ${blockingLabels.join(', ')}`); - - await getUI().showBlockingOutage(readiness); - - // The TUI lets the user continue past an outage; non-interactive runs - // (CI) do the same automatically — the degraded services are reported - // above, but we proceed rather than aborting on a transient upstream blip. - if (!isNonInteractiveEnvironment()) { - await wizardAbort({ - code: ErrorCodes.EnvServiceOutage, - message: - 'Cannot start — external services are down:\n' + - blockingLabels.map((l) => ` - ${l}`).join('\n') + - '\n\nPlease try again later.', - }); - } - } else if (readiness.decision === WizardReadiness.YesWithWarnings) { - getUI().setReadinessWarnings(readiness); - } - } - - // 3. Settings conflicts - const settingsConflicts = checkAllSettingsConflicts(session.installDir); - logToFile( - `[agent-runner] settings conflicts: ${ - settingsConflicts.length > 0 - ? settingsConflicts - .map((c) => `${c.source}(${c.keys.join(',')})`) - .join('; ') - : 'none' - }`, + `mcp=${mcpUrlFor(input.flags.localMcp)} ` + + `posthog=${input.host.baseUrl ?? 'region-resolved'}`, ); - if (settingsConflicts.length > 0) { - for (const conflict of settingsConflicts) { - const level = conflict.source === 'managed' ? 'org' : conflict.source; - analytics.wizardCapture('settings conflict detected', { - level, - keys: conflict.keys, - }); - } - - const { autoFix, failClosed, warnOnly } = - classifySettingsConflicts(settingsConflicts); - - // User-global and project-local files are already neutralized — the agent - // runs with settingSources:['project'], so the SDK never reads them. Record - // it and move on; don't make the user act on a setting that can't bite. - for (const conflict of warnOnly) { - logToFile( - `[agent-runner] settings conflict in ${conflict.source} (${conflict.path}) ` + - `neutralized by settingSources:['project'] — not blocking`, - ); - analytics.wizardCapture('settings conflict neutralized', { - level: conflict.source, - keys: conflict.keys, - }); - } - - // Writable project settings.json — the SDK *does* read it, but we can back - // it up and remove it (restored at outro). Neutralize without prompting. - let unfixable = failClosed; - if (autoFix.length > 0) { - const fixed = backupAndFixClaudeSettings(session.installDir); - if (fixed) { - logToFile('[agent-runner] auto-neutralized writable settings conflict'); - analytics.wizardCapture('settings conflict auto-neutralized', { - keys: autoFix.flatMap((c) => c.keys), - }); - } else { - // Couldn't remove it — don't run into the redirect; fail closed instead. - logToFile( - '[agent-runner] could not back up writable settings conflict — failing closed', - ); - unfixable = [...failClosed, ...autoFix]; - } - } - - // What we cannot neutralize (org-managed, always read by the SDK; or a - // writable file we failed to back up) must be fixed by the user. Fail - // closed: the screen names the file + keys and exits. - if (unfixable.length > 0) { - if (isNonInteractiveEnvironment()) { - await wizardAbort({ - code: ErrorCodes.SettingsUnfixableConflict, - message: - 'Cannot start — a Claude settings file redirects the agent away ' + - 'from the PostHog gateway and cannot be neutralized automatically:\n' + - unfixable - .map((c) => ` - ${c.source} (${c.path}): ${c.keys.join(', ')}`) - .join('\n') + - '\n\nRemove the conflicting keys and re-run the wizard.', - }); - } - await getUI().showSettingsOverride(unfixable, () => - backupAndFixClaudeSettings(session.installDir), - ); - logToFile('[agent-runner] settings override resolved'); - } - } - - analytics.wizardCapture('agent started', { - integration: config.integrationLabel, - program_id: programConfig.id, - skill_id: config.skillId ?? null, - }); - - // 4. Authenticate — idempotent within a run (see authenticate()). A second - // agent run in the same invocation (self-driving's integration phase) reuses - // the first login; it does not launch another OAuth. authenticate() also - // identifies the user and sets analytics groups. - await authenticate(session, programConfig.id); - maybeStampAiSdkDetected(session); - const project = session.apiProject; - - // 4.5. AI opt-in enforcement. Parks here while AiOptInRequiredScreen is - // up if the org hasn't approved third-party AI — BEFORE the skill - // install and agent start, so no source leaves the machine. The screen - // alone is cosmetic; this await is the actual gate. Resolves - // immediately when the program declared requiresAi: false or in CI. - // In bootstrapProgram so both the linear and orchestrator arms gate. - logToFile('[agent-runner] checking AI opt-in gate'); - await getUI().waitForAiOptIn(); - logToFile('[agent-runner] AI opt-in gate cleared'); - - // 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. - for (const step of postAuthGateSteps(programConfig.steps)) { - logToFile(`[agent-runner] awaiting post-auth gate: ${step.id}`); - await getUI().waitForGate(step.id); - logToFile(`[agent-runner] post-auth gate cleared: ${step.id}`); - } - - // Feature flags. Both arms need these, and the fork decision reads the flags. - // This map is PostHog-side only — CLI `--harness` / `--sequence` precedence - // lives at the resolution sites (`runner/index.ts` for sequence, - // `resolveHarness` for harness), not here. - const wizardFlags = await analytics.getAllFlagsForWizard(); - const wizardFlagPayloads = analytics.getWizardFlagPayloads(); - - // Gateway trace tags for this run. The runner stamps its variant onto this - // after the fork (see runProgram), so the value reflects which arm ran. - const wizardMetadata = buildRunTags({ - programId: programConfig.id, - integration: config.integrationLabel, - runId: analytics.runId, - build: analytics.build, - skillId: config.skillId, - }); - - // The agent can't swap tokens mid-run, so freshness is measured after every park above, right before the mint. - await refreshAccessTokenIfNeeded(session); - - // Credentials (incl. the resolved host family and its MCP url) live on - // `session.credentials`; narrow once at this boundary — `authenticate` above - // set them — so downstream readers get a non-null type without asserting. - const credentials = session.credentials!; + const { credentials } = input; + const { wizardFlags, wizardFlagPayloads, wizardMetadata, programId } = config; // Mint now so a refusal fails the boot before any agent starts. Later // readers re-resolve through the cache, which re-mints past the refresh // point. const currentGatewayAuth = () => - gatewayAuth(credentials.host, credentials.accessToken, programConfig.id); + gatewayAuth(credentials.host, credentials.accessToken, programId); await currentGatewayAuth(); return { @@ -316,33 +106,24 @@ export async function bootstrapProgram( credentials, // Carried so per-task sessions re-resolve against the same program the boot // minted for, rather than digging it back out of the metadata bag. - programId: programConfig.id, + programId, wizardFlags, wizardFlagPayloads, wizardMetadata, - project, - // Resolved once, here: the only place holding both the switchboard inputs + project: input.project, + // Resolved once, here: the only place holding both the run-level harness // and the gateway auth. Every skill install downstream reads it off boot. - triageProvider: createTriageLLMProvider( - async () => { - const auth = await currentGatewayAuth(); - return { - baseURL: auth.gatewayUrl, - authToken: auth.token, - teamId: auth.teamId, - // `call_type` splits scan spend out of the program's agent cost, - // the same tag the in-run triage provider carries. - wizardMetadata: { ...wizardMetadata, call_type: CallType.yaraTriage }, - wizardFlags, - }; - }, - resolveHarness({ - program: programConfig.id, - flags: wizardFlags, - flagPayloads: wizardFlagPayloads, - cliHarness: session.harness, - cliModel: session.model, - }).harness, - ), + triageProvider: createTriageLLMProvider(async () => { + const auth = await currentGatewayAuth(); + return { + baseURL: auth.gatewayUrl, + authToken: auth.token, + teamId: auth.teamId, + // `call_type` splits scan spend out of the program's agent cost, + // the same tag the in-run triage provider carries. + wizardMetadata: { ...wizardMetadata, call_type: CallType.yaraTriage }, + wizardFlags, + }; + }, config.binding.harness), }; } diff --git a/src/lib/agent/runner/shared/errors.ts b/src/lib/agent/runner/shared/errors.ts index 376cb45d4..c012b59a8 100644 --- a/src/lib/agent/runner/shared/errors.ts +++ b/src/lib/agent/runner/shared/errors.ts @@ -4,14 +4,14 @@ import type { InstallSkillResult } from '@lib/wizard-tools'; import { skillErrorCode } from '@lib/errors'; -import { wizardAbort, WizardError } from '@utils/wizard-abort'; +import { WizardError } from '@utils/wizard-abort'; +import type { AgentFailure } from './types'; -export async function abortOnInstallFailure( +/** The failure a skill install error decides. The caller reports and exits. */ +export function installFailure( integrationLabel: string, - result: InstallSkillResult, -): Promise { - if (result.kind === 'ok') return; - + result: Exclude, +): AgentFailure { const code = skillErrorCode(result) ?? undefined; const message = (() => { @@ -25,7 +25,7 @@ export async function abortOnInstallFailure( } })(); - await wizardAbort({ + return { message, code, error: new WizardError( @@ -40,5 +40,5 @@ export async function abortOnInstallFailure( }, code, ), - }); + }; } diff --git a/src/lib/agent/runner/shared/progress-collector.ts b/src/lib/agent/runner/shared/progress-collector.ts new file mode 100644 index 000000000..ee10ff7d8 --- /dev/null +++ b/src/lib/agent/runner/shared/progress-collector.ts @@ -0,0 +1,117 @@ +/** + * The agent's own record of what it reported. + * + * `runAgent` accumulates its final `RunSnapshot` here, independently of any + * observer, so a missing or throwing `onProgress` never makes the result + * incomplete. The emitter wraps the caller's callback: it applies the event + * here first, then hands a copy to the observer, and logs rather than + * propagates anything the observer throws. + */ + +import { logToFile } from '@utils/debug'; +import type { + AgentProgress, + ProgressEmitter, + SpinnerHandle, +} from '@lib/agent/progress'; +import type { RunSnapshot } from './types'; + +export interface ProgressCollector { + emit: ProgressEmitter; + snapshot(): RunSnapshot; +} + +export function createProgressCollector( + onProgress?: (event: AgentProgress) => void, +): ProgressCollector { + const snapshot: RunSnapshot = { + tasks: [], + statusMessages: [], + usage: { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + }, + }; + + const apply = (event: AgentProgress): void => { + switch (event.kind) { + case 'tasks': + snapshot.tasks = event.tasks.map((t) => ({ ...t })); + break; + case 'status': + snapshot.statusMessages.push(event.message); + break; + case 'stage': + snapshot.stage = event.stage; + break; + case 'url': + if (event.which === 'dashboard') snapshot.dashboardUrl = event.url; + else snapshot.notebookUrl = event.url; + break; + case 'usage': + snapshot.usage.inputTokens += event.delta.inputTokens; + snapshot.usage.outputTokens += event.delta.outputTokens; + snapshot.usage.cacheReadTokens += event.delta.cacheReadTokens; + snapshot.usage.cacheCreationTokens += event.delta.cacheCreationTokens; + break; + case 'finalCost': + snapshot.finalCostUsd = event.usd; + break; + default: + break; + } + }; + + const emit: ProgressEmitter = (event) => { + apply(event); + if (!onProgress) return; + try { + onProgress(event); + } catch (error) { + // A broken projection is the host's problem, not the run's. Say so in + // the log and carry on; the snapshot above is the source of truth. + logToFile( + `[agent] progress observer threw on ${event.kind}:`, + error instanceof Error ? error.message : error, + ); + } + }; + + return { + emit, + snapshot: () => ({ + ...snapshot, + tasks: snapshot.tasks.map((t) => ({ ...t })), + statusMessages: [...snapshot.statusMessages], + usage: { ...snapshot.usage }, + }), + }; +} + +/** The run spinner as a progress emitter. One per run, like `getUI().spinner()`. */ +export function createEmitSpinner(emit: ProgressEmitter): SpinnerHandle { + return { + start: (message) => emit({ kind: 'spinner', action: 'start', message }), + stop: (message) => emit({ kind: 'spinner', action: 'stop', message }), + message: (message) => emit({ kind: 'spinner', action: 'message', message }), + }; +} + +/** `WizardUI.log` as a progress emitter. */ +export function createEmitLog(emit: ProgressEmitter): { + info(message: string): void; + warn(message: string): void; + error(message: string): void; + success(message: string): void; + step(message: string): void; +} { + return { + info: (message) => emit({ kind: 'log', level: 'info', message }), + warn: (message) => emit({ kind: 'log', level: 'warn', message }), + error: (message) => emit({ kind: 'log', level: 'error', message }), + success: (message) => emit({ kind: 'log', level: 'success', message }), + step: (message) => emit({ kind: 'log', level: 'step', message }), + }; +} diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index 4f4292f83..388695020 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -1,16 +1,31 @@ /** - * Shared types for the runner pipeline. + * The agent's run contracts. + * + * `runAgent(config, input, options)` takes resolved execution data and an + * invocation snapshot, reports through `options.onProgress`, asks through + * `options.interaction`, and returns a `RunResult`. Nothing here names a UI, + * a store, a session or a program registry: the caller resolves those and + * hands over plain data. `src/lib/programs/run-agent-legacy.ts` is the caller + * that rebuilds today's session-driven behavior on top of this contract. */ import type { - Credentials, AdditionalFeature, + CloudRegion, + Credentials, + OutroData, + TaskNotice, WizardSession, } from '@lib/wizard-session'; import type { PromptContext } from '@lib/agent/agent-prompt'; import type { PackageManagerDetector } from '@lib/detection/package-manager'; -import type { ApiProject } from '@lib/api'; +import type { ApiProject, ApiUser } from '@lib/api'; +import type { Harness, Integration, Sequence } from '@lib/constants'; +import type { ErrorCode } from '@lib/errors'; import type { LLMProvider } from '@posthog/warlock'; +import type { AgentInteraction, ProgressEmitter } from '@lib/agent/progress'; +import type { EffortLevel } from '../switchboard/models'; +import type { SwitchboardCtx } from '../switchboard'; export type { PromptContext, Credentials }; @@ -23,7 +38,7 @@ export interface AbortCase { message: string; body: string; docsUrl?: string; - errorCode?: import('@lib/errors').ErrorCode; + errorCode?: ErrorCode; } /** @@ -32,6 +47,10 @@ export interface AbortCase { * 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`. + * + * The three session-taking hooks at the end are the caller's: the agent never + * calls them. `run-agent-legacy.ts` binds them and hands the agent + * `RunConfig.hooks` instead. */ export interface ProgramRun { /** Analytics label (e.g. 'revenue-analytics-setup', 'nextjs') */ @@ -116,15 +135,132 @@ export interface ProgramRun { resolveStepKey?: (stepName: string | undefined) => string | undefined; } +/** A task the caller queues itself before the orchestrator's planner runs. */ +export interface SeedTaskEntry { + type: string; + label?: string; + inputs?: Record; + /** Shown before the run starts, letting the user decline the task. */ + notice?: TaskNotice; +} + +/** + * Completion hooks the caller binds for the agent. Each receives the run's + * resolved credentials, exactly what the linear and orchestrator sequences + * passed alongside the session before. + */ +export interface RunHooks { + /** Runs after the agent completes, before the outro (linear only). */ + postRun?: (credentials: Credentials) => Promise; + /** Custom outro data (linear only). Omit for the default outro. */ + buildOutroData?: (credentials: Credentials) => OutroData | undefined; + /** Outro bullets for the orchestrated sequence. */ + buildOutroNextSteps?: ( + credentials: Credentials, + completedSeededTypes: readonly string[], + ) => { heading: string; items: string[] } | undefined; +} + +/** The run-level routing decision the caller made. */ +export interface ResolvedBinding { + sequence: Sequence; + harness: Harness; + /** Gateway model id. */ + model: string; + /** Reasoning-effort override. Absent → the model's table default. */ + thinkingLevel?: EffortLevel; +} + +/** + * Resolved execution data for one agent run. The caller has already decided + * which program this is, how it is routed and which flags apply; the agent + * treats every label as opaque. + */ +export interface RunConfig { + /** Program id: gateway spend pin, analytics label, commandments axis. */ + programId: string; + /** The program's run definition. Its session-taking hooks are the caller's, see `hooks`. */ + run: ProgramRun; + /** A composed sub-run skips the terminal outro and the analytics shutdown. */ + composed: boolean; + /** Run-level sequence, harness and model. */ + binding: ResolvedBinding; + /** + * The inputs the run-level binding was resolved from. The orchestrator + * re-resolves the harness per task role from these; nothing else reads them. + */ + switchboard: SwitchboardCtx; + /** Primary skills origin (context-mill dev or GitHub Releases). */ + skillsBaseUrl: string; + /** Feature flag key → variant, evaluated before the run. */ + wizardFlags: Record; + /** Flag payloads from the same snapshot. */ + wizardFlagPayloads: Record; + /** Gateway trace tags for this run, already stamped with sequence and harness. */ + wizardMetadata: Record; + /** Extra tools added on top of BASE_ALLOWED_TOOLS for this run. */ + allowedTools?: readonly string[]; + /** Tools removed from BASE_ALLOWED_TOOLS for this run. */ + disallowedTools?: readonly string[]; + /** Context-mill flow the orchestrator loads. Defaults to `programId`. */ + agentFlow?: string; + /** Tasks to queue before the orchestrator's planner runs. */ + seedTasks?: () => SeedTaskEntry[]; + /** Completion hooks, bound by the caller. */ + hooks?: RunHooks; +} + +/** Invocation flags the agent reads. */ +export interface RunFlags { + ci: boolean; + signup: boolean; + debug: boolean; + /** Harness-only: keep the ask bridge in a `ci` run that has an answerer. */ + e2eAsk: boolean; + localMcp: boolean; + captureAio: boolean; + benchmark: boolean; + yaraReport: boolean; +} + +/** + * The invocation snapshot for one agent run. Taken once by the caller; the + * agent never refreshes these from a higher layer. + */ +export interface RunInput { + installDir: string; + /** Resolved credentials, including the host family and its MCP url. */ + credentials: Credentials; + /** Project payload resolved at authentication, for prompt context. */ + project: ApiProject | null; + /** User payload resolved at authentication, for the AI opt-in prompt line. */ + apiUser: ApiUser | null; + /** The skill this run is for: the run's skill id, else its integration label. */ + skillId?: string; + /** Detected framework, when the caller has one. */ + integration?: Integration | null; + /** Docs page for the detected framework, for the orchestrator's preflight message. */ + frameworkDocsUrl?: string; + flags: RunFlags; + /** Where PostHog is, as the CLI was told. */ + host: { + baseUrl?: string; + region?: CloudRegion; + email?: string; + /** `--project-id`, for the auth-error classifier. */ + projectId?: number; + /** `--api-key`, for the auth-error classifier. */ + apiKey?: string; + }; +} + /** - * Result of the shared bootstrap, consumed by both the linear and the - * orchestrator arm. `bootstrapProgram` runs `authenticate` before returning, so - * `credentials` is guaranteed non-null here — the single narrowing point owns - * the invariant, and downstream readers get a properly non-null type for free. + * The values `prepareRun` resolves from `RunConfig` + `RunInput` and hands to + * both sequences: the gateway mint and the scan-triage classifier built on it. */ export interface BootstrapResult { skillsBaseUrl: string; - /** Auth outputs (incl. the resolved host family and its MCP url), narrowed at the boundary. */ + /** Resolved credentials (incl. the host family and its MCP url). */ credentials: Credentials; /** Program this run is, and the node its gateway spend pins to. */ programId: string; @@ -137,3 +273,87 @@ export interface BootstrapResult { /** Scan-triage classifier on this run's harness. Undefined → skill scans fail closed. */ triageProvider: LLMProvider | undefined; } + +/** + * A decided failure. The same fields `wizardAbort` takes, so the legacy + * adapter passes it through untouched and the exit sequence, codes and + * messages stay exactly what they were. + */ +export interface AgentFailure { + message?: string; + /** Structured error data. Renders via `outroError` instead of `outro`. */ + outroData?: OutroData; + error?: Error; + exitCode?: number; + code?: ErrorCode; + detail?: Record; +} + +export type RunOutcome = + /** The run finished; `outro` holds what to show. */ + | 'success' + /** The agent chose to stop (`[ABORT]`); `failure` holds the rendered case. */ + | 'aborted' + /** A coded failure the agent decided; `failure` holds it. */ + | 'failed' + /** `options.signal` aborted the run. */ + | 'cancelled' + /** + * Something threw that the agent did not decide (a refused gateway mint, an + * SDK crash). `failure.error` is the original error and `failure.code` its + * classification, so a caller can report it or rethrow it as it did before. + */ + | 'crashed'; + +/** Totals of every `usage` event the run emitted. */ +export interface TokenUsageTotals { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheCreationTokens: number; +} + +/** What the agent reported, accumulated independently of any observer. */ +export interface RunSnapshot { + tasks: import('@lib/agent/progress').TaskSnapshot[]; + statusMessages: string[]; + stage?: string; + usage: TokenUsageTotals; + finalCostUsd?: number; + dashboardUrl?: string; + notebookUrl?: string; +} + +/** + * `runAgent` never rejects: every ending is one of these, and every ending + * that is not `success` carries a `failure` the caller can act on. + */ +export interface RunResult { + outcome: RunOutcome; + /** The skill this run installed or was for. */ + skillId?: string; + /** Success outro. Absent for a composed sub-run, which has no terminal outro. */ + outro?: OutroData; + /** Present whenever `outcome` is not `success`. */ + failure?: AgentFailure; + snapshot: RunSnapshot; +} + +export interface RunAgentOptions { + /** Receives every progress event in emission order. Never awaited. */ + onProgress?: (event: import('@lib/agent/progress').AgentProgress) => void; + /** Answers the agent's questions. Absent → no ask bridge, notices declined. */ + interaction?: AgentInteraction; + /** Cancels the run at its next phase boundary and any pending question. */ + signal?: AbortSignal; +} + +/** What a sequence receives: the contracts plus the prepared run. */ +export interface SequenceContext { + config: RunConfig; + input: RunInput; + boot: BootstrapResult; + emit: ProgressEmitter; + interaction: AgentInteraction | undefined; + signal: AbortSignal; +} diff --git a/src/lib/agent/runner/switchboard/sequence.ts b/src/lib/agent/runner/switchboard/sequence.ts index e39e228e8..521eda57b 100644 --- a/src/lib/agent/runner/switchboard/sequence.ts +++ b/src/lib/agent/runner/switchboard/sequence.ts @@ -12,9 +12,7 @@ import { resolveFlagSequence, } from './flags'; import { getHarness, resolveHarness } from './harness'; -import type { WizardSession } from '@lib/wizard-session'; -import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun, BootstrapResult } from '../shared/types'; +import type { RunResult, SequenceContext } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; import { @@ -29,26 +27,18 @@ import { export interface SequenceRunner { readonly name: Sequence; - run( - session: WizardSession, - config: ProgramRun, - programConfig: ProgramConfig, - boot: BootstrapResult, - /** Composed sub-run (integration inside self-driving); linear-only. */ - composed: boolean, - ): Promise; + /** Run one program to a decided result. Unexpected errors propagate. */ + run(ctx: SequenceContext): Promise; } export const SEQUENCE_OPTIONS: Partial> = { [Sequence.linear]: { name: Sequence.linear, - run: (session, config, programConfig, boot, composed) => - runLinearProgram(session, config, programConfig, boot, composed), + run: (ctx) => runLinearProgram(ctx), }, [Sequence.orchestrator]: { name: Sequence.orchestrator, - run: (session, config, programConfig, boot, _composed) => - runOrchestrator(session, config, programConfig, boot), + run: (ctx) => runOrchestrator(ctx), }, }; diff --git a/src/lib/detection/__tests__/project-scope.test.ts b/src/lib/detection/__tests__/project-scope.test.ts index 46aedaf04..18f9173dc 100644 --- a/src/lib/detection/__tests__/project-scope.test.ts +++ b/src/lib/detection/__tests__/project-scope.test.ts @@ -10,12 +10,12 @@ import { AGENTIC_DETECTION_TIMEOUT_MS, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, } from '@lib/constants'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { buildSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; // Mock only the two network edges of scopeInstallDirToProject; everything else runs real. -vi.mock('@lib/agent/runner/shared/authenticate', () => ({ +vi.mock('@lib/programs/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), })); vi.mock('@lib/detection/agentic', async (importOriginal) => ({ diff --git a/src/lib/detection/project-scope.ts b/src/lib/detection/project-scope.ts index 73279b075..da4ffaf0a 100644 --- a/src/lib/detection/project-scope.ts +++ b/src/lib/detection/project-scope.ts @@ -8,7 +8,7 @@ import { type DetectEvent, type DetectTarget, } from './agentic.js'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { AGENTIC_DETECTION_TIMEOUT_MS, diff --git a/src/lib/agent/runner/shared/__tests__/refresh-access-token-if-needed.test.ts b/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts similarity index 100% rename from src/lib/agent/runner/shared/__tests__/refresh-access-token-if-needed.test.ts rename to src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts diff --git a/src/lib/agent/runner/shared/authenticate.ts b/src/lib/programs/authenticate.ts similarity index 98% rename from src/lib/agent/runner/shared/authenticate.ts rename to src/lib/programs/authenticate.ts index 8644a328c..1c55453dd 100644 --- a/src/lib/agent/runner/shared/authenticate.ts +++ b/src/lib/programs/authenticate.ts @@ -11,7 +11,7 @@ */ import type { Credentials, WizardSession } from '@lib/wizard-session'; -import type { ProgramId } from '@lib/programs/program-registry'; +import type { ProgramId } from './program-registry'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { refreshAccessToken } from '@utils/oauth'; import { OAuthError } from '@utils/oauth-errors'; diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index c3ac0b913..d495b5374 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -1,5 +1,6 @@ import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; -import { runAgent, type ProgramRun } from '@lib/agent/agent-runner'; +import { runAgent } from '@lib/programs/run-agent-legacy'; +import type { ProgramRun } from '@lib/agent/agent-runner'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import type { WizardSession } from '@lib/wizard-session'; import { mayReportScanResults, OutroKind, RunPhase } from '@lib/wizard-session'; @@ -21,7 +22,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/agent/agent-runner'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; import { buildCodingAgentPrompt } from './handoff.js'; diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts new file mode 100644 index 000000000..3aac92693 --- /dev/null +++ b/src/lib/programs/run-agent-legacy.ts @@ -0,0 +1,530 @@ +/** + * The session-driven agent runner every existing caller uses. + * + * `runAgent(programConfig, session)` rebuilds today's behavior on top of the + * functional `runAgent(config, input, options)` in `@lib/agent/runner`: it + * runs the gates the TUI owns (health, settings, AI opt-in, post-auth steps), + * authenticates, resolves the program's binding, builds the agent's inputs + * from the session, maps every progress event back onto `getUI()` one call + * per event, answers the agent's questions through `getUI()`, and applies the + * result — `wizardAbort` for a decided failure, nothing more for success. + * + * This is the only file that knows about `getUI()`, the session and + * `wizardAbort` on the agent's behalf. Programs replace it in Release B. + */ + +import type { WizardSession } from '@lib/wizard-session'; +import { analytics } from '@utils/analytics'; +import { getUI, type WizardUI } from '@ui'; +import type { SpinnerHandle } from '@lib/agent/progress'; +import type { AgentInteraction, AgentProgress } from '@lib/agent/progress'; +import { + runAgent as runAgentFunctional, + resolveBinding, + type ProgramRun, + type RunConfig, + type RunInput, + type SwitchboardCtx, +} from '@lib/agent/runner'; +import type { ProgramBinding } from '@lib/agent/runner/switchboard'; +import { buildRunTags } from '@lib/agent/agent-interface'; +import { + backupAndFixClaudeSettings, + checkAllSettingsConflicts, + classifySettingsConflicts, + restoreClaudeSettings, +} from '@lib/agent/claude-settings'; +import { flushScanReport } from '@lib/yara-hooks'; +import { + evaluateWizardReadiness, + WizardReadiness, + SIGNUP_WIZARD_READINESS_CONFIG, + getBlockingServiceKeys, + SERVICE_LABELS, +} from '@lib/health-checks/readiness'; +import { enableDebugLogs, logToFile, initLogFile } from '@utils/debug'; +import { registerCleanup, wizardAbort } from '@utils/wizard-abort'; +import { ErrorCodes } from '@lib/errors'; +import { isNonInteractiveEnvironment } from '@utils/environment'; +import { + getSkillsBaseUrl, + Sequence, + WIZARD_ORCHESTRATOR_FLAG_KEY, + WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, + type Integration, +} from '@lib/constants'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { postAuthGateSteps, type ProgramConfig } from './program-step'; +import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; +import { maybeStampAiSdkDetected } from './posthog-integration/detect'; +import { startAuditLedgerWatcher } from './audit/ledger-watcher'; + +/** + * Resolve a ProgramConfig's agent run definition and execute the pipeline. + * Entry point for the runners and for composed run steps. + */ +export async function runAgent( + programConfig: ProgramConfig, + session: WizardSession, + options: { composed?: boolean } = {}, +): Promise { + if (!programConfig.run) { + throw new Error(`Program "${programConfig.id}" has no run configuration.`); + } + + // Before `run()` resolves: an audit seeds the ledger from inside its recipe, + // and a watcher started later would ignore that write as pre-existing. + const ledger = programConfig.auditLedgerFile + ? startAuditLedgerWatcher(session.installDir, programConfig.auditLedgerFile) + : null; + if (ledger) registerCleanup(() => ledger.stop()); + + try { + const runDef = + typeof programConfig.run === 'function' + ? await programConfig.run(session) + : programConfig.run; + + await runProgram(session, runDef, programConfig, options.composed ?? false); + } finally { + ledger?.stop(); + } +} + +/** + * Gates → authenticate → flags → binding → the functional run → apply result. + * Every step happens in the order it did inside the agent's bootstrap. + */ +async function runProgram( + session: WizardSession, + run: ProgramRun, + programConfig: ProgramConfig, + composed: boolean, +): Promise { + // 1. Init logging + debug + initLogFile(); + session.skillId = run.skillId ?? run.integrationLabel; + logToFile( + `[agent-runner] START ${run.integrationLabel} build=${analytics.build}` + + `${session.ci ? ' (non-interactive)' : ''}`, + ); + if (session.debug) { + enableDebugLogs(); + } + + // 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. + await runHealthGate(session, programConfig); + + // 3. Settings conflicts + await runSettingsGate(session); + + analytics.wizardCapture('agent started', { + integration: run.integrationLabel, + program_id: programConfig.id, + skill_id: run.skillId ?? null, + }); + + // 4. Authenticate — idempotent within a run (see authenticate()). A second + // agent run in the same invocation (self-driving's integration phase) reuses + // the first login; it does not launch another OAuth. authenticate() also + // identifies the user and sets analytics groups. + await authenticate(session, programConfig.id); + maybeStampAiSdkDetected(session); + + // 4.5. AI opt-in enforcement. Parks here while AiOptInRequiredScreen is + // up if the org hasn't approved third-party AI — BEFORE the skill + // install and agent start, so no source leaves the machine. The screen + // alone is cosmetic; this await is the actual gate. Resolves + // immediately when the program declared requiresAi: false or in CI. + logToFile('[agent-runner] checking AI opt-in gate'); + await getUI().waitForAiOptIn(); + logToFile('[agent-runner] AI opt-in gate cleared'); + + // 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. + for (const step of postAuthGateSteps(programConfig.steps)) { + logToFile(`[agent-runner] awaiting post-auth gate: ${step.id}`); + await getUI().waitForGate(step.id); + logToFile(`[agent-runner] post-auth gate cleared: ${step.id}`); + } + + // Feature flags. Both arms need these, and the routing decision reads them. + const wizardFlags = await analytics.getAllFlagsForWizard(); + const wizardFlagPayloads = analytics.getWizardFlagPayloads(); + + // Gateway trace tags for this run; the binding below stamps its axes on. + const wizardMetadata = buildRunTags({ + programId: programConfig.id, + integration: run.integrationLabel, + runId: analytics.runId, + build: analytics.build, + skillId: run.skillId, + }); + + // The agent can't swap tokens mid-run, so freshness is measured after every + // park above, right before the agent mints. + await refreshAccessTokenIfNeeded(session); + + // Credentials (incl. the resolved host family and its MCP url) live on + // `session.credentials`; narrow once at this boundary — `authenticate` above + // set them — so downstream readers get a non-null type without asserting. + const credentials = session.credentials!; + + // Resolve which sequence and harness will run a program (CLI → PostHog flag → + // per-program binding → default), tag both axes onto analytics, and hand the + // binding to the agent for dispatch. + const switchboard: SwitchboardCtx = { + program: programConfig.id, + composed, + flags: wizardFlags, + flagPayloads: wizardFlagPayloads, + cliHarness: session.harness, + cliSequence: session.sequence, + cliModel: session.model, + }; + const binding = resolveBinding(switchboard); + analytics.setTag('sequence', binding.sequence); + analytics.setTag('harness', binding.harness); + wizardMetadata.SEQUENCE = binding.sequence; + wizardMetadata.HARNESS = binding.harness; + captureSwitchboardDecision(switchboard, binding); + + const ui = getUI(); + + // Cleanup coverage for the abort/cancel path: `wizardAbort` runs the + // registered cleanups, and the agent's own `finally` covers completion. + // flushScanReport is idempotent, so the overlap is a harmless no-op. + registerCleanup(() => flushScanReport({ yaraReport: session.yaraReport })); + + // Linear settings restoration fires on entry to the outro screen, so it + // is registered before the run can reach that screen. Same owner, same + // timing as before; the abort path still restores through the cleanup + // `backupAndFixClaudeSettings` registered. + if (binding.sequence === Sequence.linear) { + ui.onEnterScreen('outro', () => restoreClaudeSettings(session.installDir)); + } + + const framework = session.integration ?? session.skillId ?? undefined; + const config: RunConfig = { + programId: programConfig.id, + run, + composed, + binding, + switchboard, + skillsBaseUrl: getSkillsBaseUrl(), + wizardFlags, + wizardFlagPayloads, + wizardMetadata, + allowedTools: programConfig.allowedTools, + disallowedTools: programConfig.disallowedTools, + agentFlow: programConfig.agentFlow, + seedTasks: programConfig.seedTasks + ? () => programConfig.seedTasks!(session) + : undefined, + hooks: { + postRun: run.postRun + ? (creds) => run.postRun!(session, creds) + : undefined, + buildOutroData: run.buildOutroData + ? (creds) => run.buildOutroData!(session, creds) ?? undefined + : undefined, + buildOutroNextSteps: run.buildOutroNextSteps + ? (creds, completed) => + run.buildOutroNextSteps!(session, creds, completed) + : undefined, + }, + }; + const input: RunInput = { + installDir: session.installDir, + credentials, + project: session.apiProject, + apiUser: session.apiUser, + skillId: session.skillId ?? undefined, + integration: session.integration, + frameworkDocsUrl: framework + ? FRAMEWORK_REGISTRY[framework as Integration]?.metadata.docsUrl + : undefined, + flags: { + ci: session.ci, + signup: session.signup, + debug: session.debug, + e2eAsk: session.e2eAsk, + localMcp: session.localMcp, + captureAio: session.captureAio, + benchmark: session.benchmark, + yaraReport: session.yaraReport, + }, + host: { + baseUrl: session.baseUrl, + region: session.region, + email: session.email, + projectId: session.projectId, + apiKey: session.apiKey, + }, + }; + + const result = await runAgentFunctional(config, input, { + onProgress: createUiReducer(ui), + interaction: uiInteraction(ui), + }); + + // Success already rendered through the reducer (outro data, outro line) and + // shut analytics down. A decided failure exits exactly as it always has. An + // error the agent did not decide used to propagate out of this call, and the + // runners handle it (mint-failure screen, classifyRunFailure): keep throwing + // the original error for them. + if (result.outcome === 'crashed') { + throw result.failure?.error ?? new Error(result.failure?.message); + } + if (result.outcome !== 'success') { + await wizardAbort(result.failure); + } +} + +// ── Gates ───────────────────────────────────────────────────────────── + +async function runHealthGate( + session: WizardSession, + programConfig: ProgramConfig, +): Promise { + const hasHealthCheckScreen = programConfig.steps.some( + (s) => s.screenId === 'health-check', + ); + if (session.readinessResult) { + logToFile( + `[agent-runner] readiness pre-computed by TUI: decision=${session.readinessResult.decision}` + + `${ + session.outageDismissed ? ' (outage dismissed by user)' : '' + } — skipping re-check`, + ); + } + if (!hasHealthCheckScreen || session.readinessResult) return; + + logToFile('[agent-runner] evaluating wizard readiness'); + const readinessConfig = session.signup + ? SIGNUP_WIZARD_READINESS_CONFIG + : undefined; + const readiness = await evaluateWizardReadiness(readinessConfig); + logToFile(`[agent-runner] readiness=${readiness.decision}`); + if (readiness.decision === WizardReadiness.No) { + const blockingKeys = getBlockingServiceKeys( + readiness.health, + readinessConfig, + ); + const blockingLabels = blockingKeys.map( + (k) => `${SERVICE_LABELS[k]} (${readiness.health[k].status})`, + ); + logToFile(`[agent-runner] blocked by: ${blockingLabels.join(', ')}`); + + await getUI().showBlockingOutage(readiness); + + // The TUI lets the user continue past an outage; non-interactive runs + // (CI) do the same automatically — the degraded services are reported + // above, but we proceed rather than aborting on a transient upstream blip. + if (!isNonInteractiveEnvironment()) { + await wizardAbort({ + code: ErrorCodes.EnvServiceOutage, + message: + 'Cannot start — external services are down:\n' + + blockingLabels.map((l) => ` - ${l}`).join('\n') + + '\n\nPlease try again later.', + }); + } + } else if (readiness.decision === WizardReadiness.YesWithWarnings) { + getUI().setReadinessWarnings(readiness); + } +} + +async function runSettingsGate(session: WizardSession): Promise { + const settingsConflicts = checkAllSettingsConflicts(session.installDir); + logToFile( + `[agent-runner] settings conflicts: ${ + settingsConflicts.length > 0 + ? settingsConflicts + .map((c) => `${c.source}(${c.keys.join(',')})`) + .join('; ') + : 'none' + }`, + ); + if (settingsConflicts.length === 0) return; + + for (const conflict of settingsConflicts) { + const level = conflict.source === 'managed' ? 'org' : conflict.source; + analytics.wizardCapture('settings conflict detected', { + level, + keys: conflict.keys, + }); + } + + const { autoFix, failClosed, warnOnly } = + classifySettingsConflicts(settingsConflicts); + + // User-global and project-local files are already neutralized — the agent + // runs with settingSources:['project'], so the SDK never reads them. Record + // it and move on; don't make the user act on a setting that can't bite. + for (const conflict of warnOnly) { + logToFile( + `[agent-runner] settings conflict in ${conflict.source} (${conflict.path}) ` + + `neutralized by settingSources:['project'] — not blocking`, + ); + analytics.wizardCapture('settings conflict neutralized', { + level: conflict.source, + keys: conflict.keys, + }); + } + + // Writable project settings.json — the SDK *does* read it, but we can back + // it up and remove it (restored at outro). Neutralize without prompting. + let unfixable = failClosed; + if (autoFix.length > 0) { + const fixed = backupAndFixClaudeSettings(session.installDir); + if (fixed) { + logToFile('[agent-runner] auto-neutralized writable settings conflict'); + analytics.wizardCapture('settings conflict auto-neutralized', { + keys: autoFix.flatMap((c) => c.keys), + }); + } else { + // Couldn't remove it — don't run into the redirect; fail closed instead. + logToFile( + '[agent-runner] could not back up writable settings conflict — failing closed', + ); + unfixable = [...failClosed, ...autoFix]; + } + } + + // What we cannot neutralize (org-managed, always read by the SDK; or a + // writable file we failed to back up) must be fixed by the user. Fail + // closed: the screen names the file + keys and exits. + if (unfixable.length > 0) { + if (isNonInteractiveEnvironment()) { + await wizardAbort({ + code: ErrorCodes.SettingsUnfixableConflict, + message: + 'Cannot start — a Claude settings file redirects the agent away ' + + 'from the PostHog gateway and cannot be neutralized automatically:\n' + + unfixable + .map((c) => ` - ${c.source} (${c.path}): ${c.keys.join(', ')}`) + .join('\n') + + '\n\nRemove the conflicting keys and re-run the wizard.', + }); + } + await getUI().showSettingsOverride(unfixable, () => + backupAndFixClaudeSettings(session.installDir), + ); + logToFile('[agent-runner] settings override resolved'); + } +} + +// ── Switchboard telemetry ───────────────────────────────────────────── + +/** + * One event + one log line per run: what entered the switchboard, which + * precedence rung decided each axis, and the final pick. + */ +function captureSwitchboardDecision( + ctx: SwitchboardCtx, + binding: ProgramBinding, +): void { + const trace = ctx.trace ?? {}; + // Unpinned orchestrator runs choose a model per task from the context-mill agent prompts; the orchestrator logs that map once the prompts load. + const perTaskModel = + binding.sequence === Sequence.orchestrator && trace.model === 'binding'; + const model = perTaskModel ? 'chosen-per-task' : binding.model; + const modelSource = perTaskModel ? 'agent-prompts' : trace.model; + analytics.wizardCapture('switchboard resolved', { + program: ctx.program, + flag_self_driving_use_pi_harness: + ctx.flags[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY], + flag_self_driving_pi_payload: JSON.stringify( + ctx.flagPayloads?.[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY] ?? null, + ), + flag_orchestrator: ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY], + cli_harness: ctx.cliHarness, + cli_sequence: ctx.cliSequence, + cli_model: ctx.cliModel, + harness_source: trace.harness, + model_source: modelSource, + sequence_source: trace.sequence, + harness: binding.harness, + model, + thinking_level: binding.thinkingLevel, + sequence: binding.sequence, + }); + logToFile( + `[switchboard] decision: program=${ctx.program}` + + ` in(orchestrator=${ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY] ?? '-'},` + + ` cli=${ctx.cliHarness ?? '-'}/${ctx.cliSequence ?? '-'}/${ + ctx.cliModel ?? '-' + })` + + ` → harness=${binding.harness} (${trace.harness ?? '?'})` + + ` model=${model} (${modelSource ?? '?'})` + + ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, + ); +} + +// ── Progress → WizardUI, one call per event ─────────────────────────── + +/** + * The inverse of the agent's former `getUI()` calls: one event, one + * `WizardUI` method, synchronous, in emission order. Because each case maps + * back to exactly the call the agent used to make, the frame and flow goldens + * hold without regeneration. + */ +export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { + let spinner: SpinnerHandle | undefined; + return (event) => { + switch (event.kind) { + case 'lifecycle': + if (event.phase === 'started') ui.startRun(); + else ui.outro(event.message); + break; + case 'spinner': { + const handle = (spinner ??= ui.spinner()); + handle[event.action](event.message); + break; + } + case 'log': + ui.log[event.level](event.message); + break; + case 'status': + ui.pushStatus(event.message); + break; + case 'tasks': + ui.syncTodos(event.tasks); + break; + case 'stage': + ui.setStage(event.stage); + break; + case 'url': + if (event.which === 'dashboard') ui.setDashboardUrl(event.url); + else ui.setNotebookUrl(event.url); + break; + case 'usage': + ui.addTokenUsage(event.delta); + break; + case 'finalCost': + ui.setFinalTokenCostUsd(event.usd); + break; + case 'authError': + ui.showAuthError(event.detail); + break; + case 'completion': + ui.setOutroData(event.outro); + break; + } + }; +} + +/** The agent's questions, answered wherever `getUI()` answers them today. */ +export function uiInteraction(ui: WizardUI): AgentInteraction { + return { + ask: (question) => ui.requestQuestion(question), + cancelAsk: () => ui.cancelPendingQuestion(), + taskNotice: (notice) => ui.showTaskNotice(notice), + cancelTaskNotice: () => ui.cancelTaskNotice(), + }; +} diff --git a/src/lib/runners/__tests__/mint-recovery.test.ts b/src/lib/runners/__tests__/mint-recovery.test.ts index 24f9b5d83..076bf9a20 100644 --- a/src/lib/runners/__tests__/mint-recovery.test.ts +++ b/src/lib/runners/__tests__/mint-recovery.test.ts @@ -1,6 +1,6 @@ import { vi, it, expect, afterEach } from 'vitest'; import { runWizard } from '../run-wizard'; -import { runAgent } from '@lib/agent/agent-runner'; +import { runAgent } from '@lib/programs/run-agent-legacy'; import { startTUI } from '@ui/tui/start-tui'; import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; @@ -10,7 +10,7 @@ import { ScreenId } from '@ui/tui/router'; import { HostResolution } from '@lib/host-resolution'; import { analytics } from '@utils/analytics'; -vi.mock('@lib/agent/agent-runner', () => ({ runAgent: vi.fn() })); +vi.mock('@lib/programs/run-agent-legacy', () => ({ runAgent: vi.fn() })); vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); vi.mock('@lib/local-dev', async (original) => ({ ...(await original()), diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 7230c3ec0..a4fd1f037 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -335,7 +335,7 @@ export function runNonInteractive( } } - const { runAgent } = await import('@lib/agent/agent-runner'); + const { runAgent } = await import('@lib/programs/run-agent-legacy'); await runAgent(config, session); await settleStream(RunPhase.Completed); } catch (error) { diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index 1ee2e7120..a0e0879ff 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,7 +1,7 @@ import { VERSION } from '@lib/version'; import { logToFile, getLogFilePath } from '@utils/debug'; -import { runAgent } from '@lib/agent/agent-runner'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { runAgent } from '@lib/programs/run-agent-legacy'; +import { authenticate } from '@lib/programs/authenticate'; import { getProgramConfig } from '@lib/programs/program-registry'; import { getAuditChecks } from '@lib/programs/audit/types'; import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; diff --git a/vitest.config.ts b/vitest.config.ts index 7985ed29d..0a0de520d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -52,7 +52,6 @@ const HARNESS_TESTS = [ `e2e-harness/${TESTS}`, 'e2e-harness/**/*.{test,spec}.{js,jsx,ts,tsx}', ]; -const ARCH_TESTS = ['src/__tests__/architecture/**/*.{ts,tsx}']; const EXCLUDE = [ '**/node_modules/**', '**/dist/**', @@ -105,13 +104,12 @@ export default defineConfig({ project('tui', TUI_TESTS), project('cli', CLI_TESTS), project('harness', HARNESS_TESTS), - project('architecture', ARCH_TESTS), project( 'legacy', // The second glob keeps the pre-split behavior: a test file outside // a __tests__ directory still runs, here, rather than nowhere. [`src/**/${TESTS}`, 'src/**/*.{test,spec}.{js,jsx,ts,tsx}'], - [...AGENT_TESTS, ...TUI_TESTS, ...CLI_TESTS, ...ARCH_TESTS], + [...AGENT_TESTS, ...TUI_TESTS, ...CLI_TESTS], ), ], coverage: { From ee39d2c3587349dca699ede77eeb269e4bdbba39 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 18:15:47 -0400 Subject: [PATCH 02/61] chore(test): keep the architecture test, record the adapter edges Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- package.json | 1 + .../architecture/import-boundaries.test.ts | 403 ++++++++++++++++++ .../architecture/known-violations.json | 80 ++++ vitest.config.ts | 4 +- 4 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/architecture/import-boundaries.test.ts create mode 100644 src/__tests__/architecture/known-violations.json diff --git a/package.json b/package.json index 4a2b82474..4df515d69 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "test:tui": "vitest run --project tui", "test:cli": "vitest run --project cli", "test:harness": "vitest run --project harness", + "test:arch": "vitest run --project architecture", "test:goldens:update": "vitest run -u", "test:coverage": "pnpm build && vitest run --coverage", "test:e2e": "pnpm build && ./e2e-tests/run.sh", diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts new file mode 100644 index 000000000..8220182ae --- /dev/null +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -0,0 +1,403 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +export type Surface = 'env' | 'legacy' | 'agent' | 'tui' | 'cli'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(HERE, '../../..'); + +const SURFACE_RULES: ReadonlyArray boolean]> = + [ + ['env', (p) => p === 'src/env.ts'], + [ + 'agent', + (p) => + p.startsWith('src/lib/agent/') || + p.startsWith('src/lib/middleware/') || + p === 'src/lib/gateway-session.ts' || + p === 'src/lib/yara-hooks.ts' || + p === 'src/lib/yara-policy.ts' || + p === 'src/lib/wizard-tools/mcp.ts', + ], + [ + 'tui', + (p) => + p.startsWith('src/ui/tui/') || + p === 'src/ui/logging-ui.ts' || + p === 'src/ui/headless-ui.ts' || + p === 'src/commands/factories/family-picker.tsx' || + /^src\/lib\/programs\/[^/]+\/content\//.test(p) || + /^src\/lib\/programs\/[^/]+\/tips\.ts$/.test(p), + ], + [ + 'cli', + (p) => + p === 'bin.ts' || + p === 'src/wizard.ts' || + p === 'src/telemetry.ts' || + p.startsWith('src/commands/') || + p.startsWith('src/lib/runners/'), + ], + ]; + +export function classifySurface(relPath: string): Surface { + const p = relPath.split(path.sep).join('/'); + for (const [surface, matches] of SURFACE_RULES) { + if (matches(p)) return surface; + } + return 'legacy'; +} + +export const ALLOWED_IMPORTS: Record = { + env: [], + legacy: ['env', 'legacy'], + agent: ['env', 'legacy', 'agent'], + tui: ['env', 'legacy', 'tui'], + cli: ['env', 'legacy', 'agent', 'tui', 'cli'], +}; + +const TUI_ONLY_PACKAGES = ['ink', 'react', '@inkjs/ui', 'ink-testing-library']; + +const SKIP_DIRS = new Set([ + '__tests__', + '__mocks__', + '__fixtures__', + '__snapshots__', + 'node_modules', + 'dist', + 'coverage', +]); + +const SPECIFIER_PATTERNS = [ + /import\s+(?:type\s+)?[^'"]*?from\s*['"]([^'"]+)['"]/g, + /import\s*['"]([^'"]+)['"]/g, + /export\s+(?:type\s+)?[^'"]*?from\s*['"]([^'"]+)['"]/g, + /import\(\s*['"]([^'"]+)['"]\s*\)/g, + /require\(\s*['"]([^'"]+)['"]\s*\)/g, +]; + +const REGEX_PRECEDERS = new Set([ + '\n', + '(', + ')', + ',', + '=', + ':', + '[', + '!', + '&', + '|', + '?', + '{', + '}', + ';', + '+', + '-', + '*', + '%', + '^', + '~', + '<', + '>', +]); + +function endOfString(source: string, start: number, quote: string): number { + let i = start + 1; + while (i < source.length) { + if (source[i] === '\\') { + i += 2; + continue; + } + if (source[i] === quote) return i + 1; + i++; + } + return source.length; +} + +function endOfRegex(source: string, start: number): number { + let i = start + 1; + let inClass = false; + while (i < source.length) { + const c = source[i]; + if (c === '\\') { + i += 2; + continue; + } + if (c === '\n') return start + 1; + if (c === '[') inClass = true; + else if (c === ']') inClass = false; + else if (c === '/' && !inClass) return i + 1; + i++; + } + return source.length; +} + +function stripComments(source: string): string { + let out = ''; + let prev = '\n'; + let i = 0; + while (i < source.length) { + const c = source[i]; + const next = source[i + 1]; + if (c === '/' && next === '/') { + while (i < source.length && source[i] !== '\n') i++; + continue; + } + if (c === '/' && next === '*') { + i += 2; + while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) + i++; + i += 2; + out += ' '; + continue; + } + if (c === '"' || c === "'" || c === '`') { + const end = endOfString(source, i, c); + out += source.slice(i, end); + prev = c; + i = end; + continue; + } + if (c === '/' && REGEX_PRECEDERS.has(prev)) { + const end = endOfRegex(source, i); + out += source.slice(i, end); + prev = '/'; + i = end; + continue; + } + out += c; + if (c === '\n' || !/\s/.test(c)) prev = c; + i++; + } + return out; +} + +function toRepoRelative(abs: string): string { + return path.relative(REPO_ROOT, abs).split(path.sep).join('/'); +} + +function isFile(abs: string): boolean { + return fs.statSync(abs, { throwIfNoEntry: false })?.isFile() ?? false; +} + +function collectFiles(absDir: string, into: string[]): void { + for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) { + const abs = path.join(absDir, entry.name); + if (entry.isDirectory()) { + if (!SKIP_DIRS.has(entry.name)) collectFiles(abs, into); + continue; + } + if (!entry.isFile()) continue; + if (!/\.tsx?$/.test(entry.name)) continue; + if (/\.d\.ts$/.test(entry.name) || /\.test\.tsx?$/.test(entry.name)) + continue; + into.push(toRepoRelative(abs)); + } +} + +function loadAliases(): ReadonlyArray { + const tsconfig = JSON.parse( + fs.readFileSync(path.join(REPO_ROOT, 'tsconfig.build.json'), 'utf8'), + ) as { compilerOptions?: { paths?: Record } }; + return Object.entries(tsconfig.compilerOptions?.paths ?? {}).map( + ([pattern, targets]) => [pattern, targets[0]] as const, + ); +} + +function aliasTarget( + spec: string, + aliases: ReadonlyArray, +): string | null { + for (const [pattern, target] of aliases) { + if (pattern.endsWith('*')) { + const prefix = pattern.slice(0, -1); + if (spec.startsWith(prefix)) { + return path.resolve( + REPO_ROOT, + target.slice(0, -1) + spec.slice(prefix.length), + ); + } + } else if (spec === pattern) { + return path.resolve(REPO_ROOT, target); + } + } + return null; +} + +function probe(base: string): string | null { + const candidates: string[] = []; + if (base.endsWith('.js')) { + const stem = base.slice(0, -3); + candidates.push(`${stem}.ts`, `${stem}.tsx`); + } + candidates.push( + `${base}.ts`, + `${base}.tsx`, + path.join(base, 'index.ts'), + path.join(base, 'index.tsx'), + base, + ); + return candidates.find(isFile) ?? null; +} + +function specifiersIn(text: string): string[] { + const found = new Set(); + for (const pattern of SPECIFIER_PATTERNS) { + pattern.lastIndex = 0; + let match = pattern.exec(text); + while (match !== null) { + found.add(match[1]); + match = pattern.exec(text); + } + } + return [...found]; +} + +type Analysis = { + files: string[]; + edges: string[]; + violations: ReadonlyArray<{ key: string; rule: string }>; + unresolved: string[]; +}; + +function analyze(): Analysis { + const files: string[] = ['bin.ts']; + collectFiles(path.join(REPO_ROOT, 'src'), files); + files.sort(); + + const aliases = loadAliases(); + const edges = new Set(); + const violations = new Map(); + const unresolved = new Set(); + + for (const file of files) { + const text = stripComments( + fs.readFileSync(path.join(REPO_ROOT, file), 'utf8'), + ); + const from = classifySurface(file); + const allowed = ALLOWED_IMPORTS[from]; + + for (const spec of specifiersIn(text)) { + const base = spec.startsWith('.') + ? path.resolve(REPO_ROOT, path.dirname(file), spec) + : aliasTarget(spec, aliases); + + if (base === null) { + const tuiOnly = + TUI_ONLY_PACKAGES.includes(spec) || spec.startsWith('react/'); + if (tuiOnly && from !== 'tui') { + violations.set(`${file} -> pkg:${spec}`, 'ink-outside-tui'); + } + continue; + } + + const resolved = probe(base); + if (resolved === null) { + unresolved.add(`${file} -> ${spec}`); + continue; + } + + const target = toRepoRelative(resolved); + const key = `${file} -> ${target}`; + edges.add(key); + + const to = classifySurface(target); + if (!allowed.includes(to)) violations.set(key, `matrix:${from}->${to}`); + } + } + + return { + files, + edges: [...edges].sort(), + violations: [...violations] + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([key, rule]) => ({ key, rule })), + unresolved: [...unresolved].sort(), + }; +} + +const analysis = analyze(); + +const known = ( + JSON.parse( + fs.readFileSync(path.join(HERE, 'known-violations.json'), 'utf8'), + ) as { violations: string[] } +).violations; + +if (process.env.PRINT_VIOLATIONS) { + const byRule = new Map(); + const byImporter = new Map(); + for (const { key, rule } of analysis.violations) { + byRule.set(rule, (byRule.get(rule) ?? 0) + 1); + const file = key.split(' -> ')[0]; + byImporter.set(file, (byImporter.get(file) ?? 0) + 1); + } + const lines = [ + `files scanned: ${analysis.files.length}`, + `edges: ${analysis.edges.length}`, + `violations: ${analysis.violations.length}`, + `unresolved: ${analysis.unresolved.length}`, + ...analysis.unresolved.map((u) => ` unresolved: ${u}`), + ...[...byRule] + .sort((a, b) => b[1] - a[1]) + .map(([rule, count]) => ` ${rule}: ${count}`), + 'top importers:', + ...[...byImporter] + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([file, count]) => ` ${file}: ${count}`), + ]; + process.stderr.write(`${lines.join('\n')}\n`); + process.stderr.write( + `${JSON.stringify( + { violations: analysis.violations.map((v) => v.key) }, + null, + 2, + )}\n`, + ); +} + +describe('import boundaries', () => { + it('resolves every internal specifier', () => { + expect(analysis.unresolved).toEqual([]); + }); + + it('introduces no violation outside known-violations.json', () => { + const knownSet = new Set(known); + const added = analysis.violations + .filter(({ key }) => !knownSet.has(key)) + .map(({ key, rule }) => `${key} [${rule}]`); + expect(added).toEqual([]); + }); + + it('keeps known-violations.json free of stale entries', () => { + const current = new Set(analysis.violations.map((v) => v.key)); + const stale = known.filter((key) => !current.has(key)); + expect( + stale, + 'stale entries, delete them from known-violations.json', + ).toEqual([]); + }); +}); + +describe('surface classification', () => { + it('maps representative paths to their surface', () => { + expect(classifySurface('src/env.ts')).toBe('env'); + expect(classifySurface('src/utils/analytics.ts')).toBe('legacy'); + expect(classifySurface('src/lib/agent/agent-runner.ts')).toBe('agent'); + expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); + expect(classifySurface('bin.ts')).toBe('cli'); + expect(classifySurface('src/lib/wizard-tools/mcp.ts')).toBe('agent'); + expect(classifySurface('src/lib/wizard-tools/tools.ts')).toBe('legacy'); + expect(classifySurface('src/commands/factories/family-picker.tsx')).toBe( + 'tui', + ); + expect( + classifySurface('src/lib/programs/posthog-integration/content/index.tsx'), + ).toBe('tui'); + expect( + classifySurface('src/lib/programs/posthog-integration/index.ts'), + ).toBe('legacy'); + }); +}); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json new file mode 100644 index 000000000..73e5f68e9 --- /dev/null +++ b/src/__tests__/architecture/known-violations.json @@ -0,0 +1,80 @@ +{ + "violations": [ + "src/commands/factories/family-picker.tsx -> src/commands/command.ts", + "src/env.ts -> src/lib/headless-mode.ts", + "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/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/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/run-agent-legacy.ts -> src/lib/agent/agent-interface.ts", + "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/claude-settings.ts", + "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/progress.ts", + "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/index.ts", + "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", + "src/lib/programs/run-agent-legacy.ts -> src/lib/yara-hooks.ts", + "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", + "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", + "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" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts index 0a0de520d..7985ed29d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -52,6 +52,7 @@ const HARNESS_TESTS = [ `e2e-harness/${TESTS}`, 'e2e-harness/**/*.{test,spec}.{js,jsx,ts,tsx}', ]; +const ARCH_TESTS = ['src/__tests__/architecture/**/*.{ts,tsx}']; const EXCLUDE = [ '**/node_modules/**', '**/dist/**', @@ -104,12 +105,13 @@ export default defineConfig({ project('tui', TUI_TESTS), project('cli', CLI_TESTS), project('harness', HARNESS_TESTS), + project('architecture', ARCH_TESTS), project( 'legacy', // The second glob keeps the pre-split behavior: a test file outside // a __tests__ directory still runs, here, rather than nowhere. [`src/**/${TESTS}`, 'src/**/*.{test,spec}.{js,jsx,ts,tsx}'], - [...AGENT_TESTS, ...TUI_TESTS, ...CLI_TESTS], + [...AGENT_TESTS, ...TUI_TESTS, ...CLI_TESTS, ...ARCH_TESTS], ), ], coverage: { From f7fe0fa59e3d6961d334aa37130372ce286263c2 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 18:20:30 -0400 Subject: [PATCH 03/61] chore(agent): drop the authenticate move and the README rewrite from the cut Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- scripts/tui-host.no-jest.ts | 2 +- .../architecture/known-violations.json | 2 + src/lib/agent/runner/README.md | 37 +++++-------------- .../refresh-access-token-if-needed.test.ts | 0 .../runner/shared}/authenticate.ts | 2 +- .../detection/__tests__/project-scope.test.ts | 4 +- src/lib/detection/project-scope.ts | 2 +- src/lib/programs/run-agent-legacy.ts | 9 +++-- src/lib/runners/run-wizard.ts | 2 +- 9 files changed, 23 insertions(+), 37 deletions(-) rename src/lib/{programs => agent/runner/shared}/__tests__/refresh-access-token-if-needed.test.ts (100%) rename src/lib/{programs => agent/runner/shared}/authenticate.ts (98%) diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index ead31e380..a6ceb1ce8 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -30,7 +30,7 @@ import { configureGatewayFromCIEnvironment } from '@lib/gateway-session'; import { runAgent } from '@lib/programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; import { getAuditChecks } from '@lib/programs/audit/types'; -import { authenticate } from '@lib/programs/authenticate'; +import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { logToFile } from '@utils/debug'; import { join } from 'path'; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 73e5f68e9..a2f9049bc 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -4,6 +4,7 @@ "src/env.ts -> src/lib/headless-mode.ts", "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", @@ -39,6 +40,7 @@ "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/claude-settings.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/progress.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/index.ts", + "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/shared/authenticate.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/yara-hooks.ts", "src/lib/programs/self-driving/detect.ts -> src/lib/agent/agent-runner.ts", diff --git a/src/lib/agent/runner/README.md b/src/lib/agent/runner/README.md index b9f9bde83..a96dfaf12 100644 --- a/src/lib/agent/runner/README.md +++ b/src/lib/agent/runner/README.md @@ -40,30 +40,12 @@ for the coordinated change checklist. Five layers, each with its own job. Nothing crosses layers unless it has to. **The entry point** (`index.ts`) is the front door: - -```ts -runAgent(config: RunConfig, input: RunInput, options?: { - onProgress?: (event: AgentProgress) => void; - interaction?: AgentInteraction; - signal?: AbortSignal; -}): Promise -``` - -`RunConfig` is resolved execution data: the program id, its run definition, the -binding the caller resolved, flags, run tags and tool lists. `RunInput` is the -invocation snapshot: directory, credentials, project, skill. The agent reports -through `onProgress` (one event per former UI call, copied data, never awaited) -and asks through `interaction` (an optional answerer for `wizard_ask` and task -notices). It returns a `RunResult` — outcome, outro, a failure with the same -fields `wizardAbort` takes, and a snapshot of what it reported. It never -renders, never reads a session, never exits the process and never rejects: a -decided failure is `aborted` or `failed`, an error the agent did not decide is -`crashed` with the original error attached. Types live in `shared/types.ts` and -`../progress.ts`. - -Everything the caller decides first — health and settings gates, OAuth, the AI -opt-in gate, post-auth gates, feature flags, token refresh, the binding — lives -in `src/lib/programs/run-agent-legacy.ts`, which also maps progress back onto +`runAgent(config, input, {onProgress?, interaction?, signal?}) → RunResult`. It +takes resolved execution data and an invocation snapshot (`shared/types.ts`), +reports through `onProgress` and asks through `interaction` (`../progress.ts`), +and returns every ending as a result. It never renders, reads a session or +exits. The gates, OAuth, flags and binding lookup that used to run here live in +`src/lib/programs/run-agent-legacy.ts`, which also maps progress back onto `getUI()` for today's runners. **Prepare** (`shared/bootstrap.ts`) is the on-ramp inside the agent: logging @@ -74,8 +56,7 @@ out to be linear or orchestrator, anthropic or pi, the setup is the same. fetched flags + any CLI overrides, it returns a `ProgramBinding` — which query shape (sequence), which agent SDK (harness), which model. Two independent middleware chains, one per axis, apply precedence rules (CLI > flag > program -config > default). The caller resolves the run-level binding; the orchestrator -re-resolves the harness per task role from the same inputs. +config > default). This is the only layer that makes routing decisions. **Sequences** (`sequence/`) are LLM query shapes. Once the switchboard has picked one, that sequence takes over the run and owns _how the LLM's work is @@ -115,5 +96,5 @@ Each layer is replaceable. 4. Harness drives each conversation through its SDK, using the bound model, on the PostHog LLM gateway. 5. The scan report flushes; `runAgent` returns a `RunResult`. -6. The caller applies it: an outro is already reported, a decided failure goes - to `wizardAbort`, a crash is rethrown for the runner's own handling. +6. The caller applies it: a decided failure goes to `wizardAbort`, a crash is + rethrown for the runner's own handling. diff --git a/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts b/src/lib/agent/runner/shared/__tests__/refresh-access-token-if-needed.test.ts similarity index 100% rename from src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts rename to src/lib/agent/runner/shared/__tests__/refresh-access-token-if-needed.test.ts diff --git a/src/lib/programs/authenticate.ts b/src/lib/agent/runner/shared/authenticate.ts similarity index 98% rename from src/lib/programs/authenticate.ts rename to src/lib/agent/runner/shared/authenticate.ts index 1c55453dd..8644a328c 100644 --- a/src/lib/programs/authenticate.ts +++ b/src/lib/agent/runner/shared/authenticate.ts @@ -11,7 +11,7 @@ */ import type { Credentials, WizardSession } from '@lib/wizard-session'; -import type { ProgramId } from './program-registry'; +import type { ProgramId } from '@lib/programs/program-registry'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { refreshAccessToken } from '@utils/oauth'; import { OAuthError } from '@utils/oauth-errors'; diff --git a/src/lib/detection/__tests__/project-scope.test.ts b/src/lib/detection/__tests__/project-scope.test.ts index 18f9173dc..46aedaf04 100644 --- a/src/lib/detection/__tests__/project-scope.test.ts +++ b/src/lib/detection/__tests__/project-scope.test.ts @@ -10,12 +10,12 @@ import { AGENTIC_DETECTION_TIMEOUT_MS, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, } from '@lib/constants'; -import { authenticate } from '@lib/programs/authenticate'; +import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { buildSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; // Mock only the two network edges of scopeInstallDirToProject; everything else runs real. -vi.mock('@lib/programs/authenticate', () => ({ +vi.mock('@lib/agent/runner/shared/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), })); vi.mock('@lib/detection/agentic', async (importOriginal) => ({ diff --git a/src/lib/detection/project-scope.ts b/src/lib/detection/project-scope.ts index da4ffaf0a..73279b075 100644 --- a/src/lib/detection/project-scope.ts +++ b/src/lib/detection/project-scope.ts @@ -8,7 +8,7 @@ import { type DetectEvent, type DetectTarget, } from './agentic.js'; -import { authenticate } from '@lib/programs/authenticate'; +import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { AGENTIC_DETECTION_TIMEOUT_MS, diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index 3aac92693..4d4c53612 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -55,7 +55,10 @@ import { } from '@lib/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { postAuthGateSteps, type ProgramConfig } from './program-step'; -import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; +import { + authenticate, + refreshAccessTokenIfNeeded, +} from '@lib/agent/runner/shared/authenticate'; import { maybeStampAiSdkDetected } from './posthog-integration/detect'; import { startAuditLedgerWatcher } from './audit/ledger-watcher'; @@ -474,7 +477,7 @@ function captureSwitchboardDecision( * back to exactly the call the agent used to make, the frame and flow goldens * hold without regeneration. */ -export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { +function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { let spinner: SpinnerHandle | undefined; return (event) => { switch (event.kind) { @@ -520,7 +523,7 @@ export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { } /** The agent's questions, answered wherever `getUI()` answers them today. */ -export function uiInteraction(ui: WizardUI): AgentInteraction { +function uiInteraction(ui: WizardUI): AgentInteraction { return { ask: (question) => ui.requestQuestion(question), cancelAsk: () => ui.cancelPendingQuestion(), diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index a0e0879ff..77384e6f2 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,7 +1,7 @@ import { VERSION } from '@lib/version'; import { logToFile, getLogFilePath } from '@utils/debug'; import { runAgent } from '@lib/programs/run-agent-legacy'; -import { authenticate } from '@lib/programs/authenticate'; +import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { getProgramConfig } from '@lib/programs/program-registry'; import { getAuditChecks } from '@lib/programs/audit/types'; import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; From b0a4c1e9deef2e2d85049d4c1d171003e1e244c4 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 18:55:33 -0400 Subject: [PATCH 04/61] fix(agent): preserve runner boundary parity Restore detection progress, isolate progress observers, and preserve completion and cleanup ordering. Type run outcomes and share status history handling; cover harness, sequence, and adapter boundaries. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- scripts/tui-host.no-jest.ts | 6 +- .../architecture/known-violations.json | 2 +- src/__tests__/cli.test.ts | 2 +- src/__tests__/provision-cli.test.ts | 2 +- .../__tests__/progress-collector.test.ts | 39 +++ .../__tests__/run-agent-standalone.test.ts | 232 ++++++++++++++++-- src/lib/agent/agent-runner.ts | 2 +- src/lib/agent/progress.ts | 13 +- src/lib/agent/runner/README.md | 10 +- src/lib/agent/runner/index.ts | 11 +- src/lib/agent/runner/sequence/linear.ts | 55 +---- .../orchestrator/__tests__/executor.test.ts | 44 ++++ .../__tests__/task-notice-timeout.test.ts | 40 +-- .../runner/sequence/orchestrator/executor.ts | 30 ++- .../orchestrator/orchestrator-runner.ts | 119 ++++----- src/lib/agent/runner/shared/ask.ts | 3 +- src/lib/agent/runner/shared/bootstrap.ts | 16 +- src/lib/agent/runner/shared/errors.ts | 7 +- .../agent/runner/shared/progress-collector.ts | 8 +- src/lib/agent/runner/shared/types.ts | 56 ++--- src/lib/agent/runner/switchboard/sequence.ts | 4 +- .../__tests__/agentic-progress.test.ts | 80 ++++++ src/lib/detection/agentic.ts | 4 +- .../__tests__/run-agent-legacy.test.ts | 227 +++++++++++++++++ src/lib/programs/posthog-integration/index.ts | 4 +- src/lib/programs/run-agent-legacy.ts | 89 +------ .../runners/__tests__/mint-recovery.test.ts | 6 +- src/lib/runners/run-non-interactive.ts | 8 +- src/lib/runners/run-wizard.ts | 6 +- src/lib/status-history.ts | 9 + src/ui/__tests__/agent-progress.test.ts | 129 ++++++++++ src/ui/agent-progress.ts | 71 ++++++ src/ui/tui/constants.ts | 2 +- src/ui/tui/store.ts | 20 +- 34 files changed, 1007 insertions(+), 349 deletions(-) create mode 100644 src/lib/agent/__tests__/progress-collector.test.ts create mode 100644 src/lib/detection/__tests__/agentic-progress.test.ts create mode 100644 src/lib/programs/__tests__/run-agent-legacy.test.ts create mode 100644 src/lib/status-history.ts create mode 100644 src/ui/__tests__/agent-progress.test.ts create mode 100644 src/ui/agent-progress.ts diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index a6ceb1ce8..d20bfc8cb 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -27,7 +27,7 @@ import type { Harness, Sequence } from '@lib/constants'; import { buildSession } from '@lib/wizard-session'; import { initLocalDev } from '@lib/local-dev'; import { configureGatewayFromCIEnvironment } from '@lib/gateway-session'; -import { runAgent } from '@lib/programs/run-agent-legacy'; +import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; import { getAuditChecks } from '@lib/programs/audit/types'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; @@ -334,13 +334,13 @@ async function main() { await step.run(await runSessionFor(step)); store.completeRunStep(step.id); } else if (step.screenId === 'run') { - await runAgent(programConfig, await runSessionFor(step)); + await runProgramAgent(programConfig, await runSessionFor(step)); } else if (step.isComplete) { await store.waitUntil(step.isComplete); } } } else { - await runAgent(programConfig, store.session); + await runProgramAgent(programConfig, store.session); } }; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index a2f9049bc..d83527e0a 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -38,7 +38,6 @@ "src/lib/programs/revenue-analytics/index.ts -> src/lib/programs/revenue-analytics/content/index.tsx", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/agent-interface.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/claude-settings.ts", - "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/progress.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/index.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/shared/authenticate.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", @@ -63,6 +62,7 @@ "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", "src/steps/run-prettier.ts -> src/telemetry.ts", "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", + "src/ui/agent-progress.ts -> src/lib/agent/progress.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", diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index e4ebbe951..4633470ee 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -113,7 +113,7 @@ vi.mock('../utils/wizard-abort', async (importOriginal) => ({ wizardAbort: vi.fn(), })); vi.mock('../lib/programs/run-agent-legacy', () => ({ - runAgent: vi.fn().mockResolvedValue(undefined), + runProgramAgent: vi.fn().mockResolvedValue(undefined), })); describe('CLI argument parsing', () => { diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 10b6539b2..95d547bd0 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -64,7 +64,7 @@ vi.mock('../utils/wizard-abort', async (importOriginal) => ({ wizardAbort: vi.fn(), })); vi.mock('../lib/programs/run-agent-legacy', () => ({ - runAgent: vi.fn().mockResolvedValue(undefined), + runProgramAgent: vi.fn().mockResolvedValue(undefined), })); import { provisionCommand } from '../commands/provision'; diff --git a/src/lib/agent/__tests__/progress-collector.test.ts b/src/lib/agent/__tests__/progress-collector.test.ts new file mode 100644 index 000000000..061292550 --- /dev/null +++ b/src/lib/agent/__tests__/progress-collector.test.ts @@ -0,0 +1,39 @@ +import { OutroKind } from '@lib/wizard-session'; +import { createProgressCollector } from '../runner/shared/progress-collector'; +import { MAX_STATUS_MESSAGES } from '@lib/status-history'; + +it('retains a bounded FIFO of statuses and drops consecutive duplicates', () => { + const observer = vi.fn(); + const collector = createProgressCollector(observer); + for (let i = 0; i < MAX_STATUS_MESSAGES + 3; i++) { + collector.emit({ kind: 'status', message: `status ${i}` }); + collector.emit({ kind: 'status', message: `status ${i}` }); + } + const statuses = collector.snapshot().statusMessages; + expect(statuses).toHaveLength(MAX_STATUS_MESSAGES); + expect(statuses[0]).toBe('status 3'); + expect(statuses.at(-1)).toBe(`status ${MAX_STATUS_MESSAGES + 2}`); + expect(observer).toHaveBeenCalledTimes((MAX_STATUS_MESSAGES + 3) * 2); + statuses.push('external mutation'); + expect(collector.snapshot().statusMessages).toHaveLength(MAX_STATUS_MESSAGES); +}); + +it('isolates nested completion data from a mutating observer', () => { + const outro = { + kind: OutroKind.Success, + message: 'Done', + nextSteps: { heading: 'Next', items: ['Keep the report'] }, + }; + const collector = createProgressCollector((event) => { + if (event.kind === 'completion') { + event.outro.message = 'corrupted'; + event.outro.nextSteps?.items.push('corrupted'); + } + }); + collector.emit({ kind: 'completion', outro }); + expect(outro).toEqual({ + kind: OutroKind.Success, + message: 'Done', + nextSteps: { heading: 'Next', items: ['Keep the report'] }, + }); +}); diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts index 9a08c7569..13f8bc458 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -9,15 +9,17 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { Harness, Sequence } from '@lib/constants'; +import { Harness, Sequence, DEFAULT_AGENT_MODEL } from '@lib/constants'; import { HostResolution } from '@lib/host-resolution'; import { OutroKind, type PendingQuestion } from '@lib/wizard-session'; import { AGENT_ERROR_CODE, ErrorCodes } from '@lib/errors'; import { AgentErrorType } from '@lib/agent/signals'; +import type { AgentFailure } from '@lib/agent/runner/shared/types'; import type { AgentProgress } from '@lib/agent/progress'; import type { AgentHarness, BackendRunInputs, + TaskRunInputs, } from '@lib/agent/runner/harness/types'; vi.mock('@ui', () => ({ @@ -29,6 +31,10 @@ vi.mock('@ui', () => ({ }, })); vi.mock('@utils/debug'); +vi.mock('@lib/yara-hooks', async (original) => ({ + ...(await original()), + flushScanReport: vi.fn(), +})); vi.mock('@utils/analytics', () => ({ analytics: { build: 'test', @@ -56,11 +62,33 @@ const harnessState = vi.hoisted(() => ({ result: {} as { error?: string; message?: string; failure?: unknown }, throws: undefined as Error | undefined, lastInputs: undefined as unknown, + tasks: [] as TaskRunInputs[], + selected: [] as Harness[], + taskFailure: undefined as AgentFailure | undefined, + seedFailure: undefined as AgentFailure | undefined, askQuestions: undefined as PendingQuestion['questions'] | undefined, })); vi.mock('@lib/agent/runner/switchboard/harness', () => { const fake: AgentHarness = { name: Harness.pi, + runTask(inputs: TaskRunInputs) { + harnessState.tasks.push(inputs); + const { store, currentTaskId } = inputs.orchestrator; + if (!currentTaskId) { + if (harnessState.seedFailure) + return Promise.resolve({ failure: harnessState.seedFailure }); + store.enqueue({ type: 'install' }); + } else if (harnessState.taskFailure) { + return Promise.resolve({ failure: harnessState.taskFailure }); + } else { + store.complete(currentTaskId, { + goals: 'install', + did: 'installed', + forNextAgent: 'done', + }); + } + return Promise.resolve({}); + }, async run(inputs: BackendRunInputs) { harnessState.lastInputs = inputs; const { emit, spinner } = inputs; @@ -72,6 +100,9 @@ vi.mock('@lib/agent/runner/switchboard/harness', () => { tasks: [{ content: 'Install', status: 'completed' }], }); emit({ kind: 'url', which: 'dashboard', url: 'https://d/1' }); + emit({ kind: 'url', which: 'notebook', url: 'https://n/1' }); + emit({ kind: 'stage', stage: 'Configure' }); + emit({ kind: 'finalCost', usd: 0.25 }); emit({ kind: 'usage', delta: { @@ -99,14 +130,55 @@ vi.mock('@lib/agent/runner/switchboard/harness', () => { }; return { HARNESS_OPTIONS: { [Harness.pi]: fake }, - getHarness: () => fake, - resolveHarness: () => ({ harness: Harness.pi, model: 'm' }), + getHarness: (name: Harness) => { + harnessState.selected.push(name); + return { ...fake, name }; + }, + resolveHarness: (ctx: { cliHarness?: Harness }) => ({ + harness: ctx.cliHarness ?? Harness.pi, + model: DEFAULT_AGENT_MODEL, + }), + }; +}); + +vi.mock('@lib/agent/agent-prompt-loader', async (original) => { + const actual = await original< + typeof import('@lib/agent/agent-prompt-loader') + >(); + return { + ...actual, + loadAgentRegistry: vi.fn(() => + Promise.resolve( + actual.buildRegistry( + [ + actual.parseAgentPrompt( + '---\ntype: seed\nseed: true\n---\nPlan work', + 'seed', + 'test-program', + ), + actual.parseAgentPrompt( + '---\ntype: install\n---\nInstall it', + 'install', + 'test-program', + ), + ], + 'test-program', + ), + ), + ), }; }); +vi.mock('@lib/wizard-tools', async (original) => ({ + ...(await original()), + fetchSkillMenu: vi.fn().mockResolvedValue({ categories: {} }), +})); import { runAgent } from '@lib/agent/runner'; import type { RunConfig, RunInput } from '@lib/agent/runner'; import { analytics } from '@utils/analytics'; +import { initLogFile } from '@utils/debug'; +import { flushScanReport } from '@lib/yara-hooks'; +import { QUEUE_DIR_NAME } from '../runner/sequence/orchestrator/queue'; let tmp: string; @@ -166,15 +238,152 @@ const input = (over: Partial = {}): RunInput => ({ beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'run-agent-standalone-')); harnessState.result = {}; + harnessState.tasks = []; + harnessState.selected = []; + harnessState.taskFailure = undefined; + harnessState.seedFailure = undefined; harnessState.throws = undefined; harnessState.lastInputs = undefined; harnessState.askQuestions = undefined; vi.mocked(analytics.shutdown).mockClear(); + vi.mocked(initLogFile).mockClear(); + vi.mocked(flushScanReport).mockClear(); }); afterEach(() => fs.rmSync(tmp, { recursive: true, force: true })); describe('runAgent standalone', () => { + it.each([Harness.pi, Harness.anthropic])( + 'dispatches %s through both sequence arms', + async (harness) => { + for (const sequence of [Sequence.linear, Sequence.orchestrator]) { + const result = await runAgent( + config({ + binding: { harness, sequence, model: DEFAULT_AGENT_MODEL }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: harness, + }, + }), + input(), + ); + expect(result.outcome).toBe('success'); + expect(harnessState.selected.at(-1)).toBe(harness); + if (sequence === Sequence.linear) { + expect(harnessState.tasks).toHaveLength(0); + } else { + expect(harnessState.tasks).toHaveLength(2); + expect( + harnessState.tasks[1].orchestrator.currentTaskId, + ).toBeDefined(); + expect(result.snapshot.tasks).toEqual([ + { content: 'install', activeForm: 'install', status: 'completed' }, + ]); + } + } + expect(analytics.shutdown).toHaveBeenCalledTimes(2); + expect(analytics.shutdown).toHaveBeenCalledWith('success'); + }, + ); + + it('cleans up when the seed fails before the drain starts', async () => { + const failure = { message: 'Authentication failed (401)' }; + harnessState.seedFailure = failure; + const result = await runAgent( + config({ + binding: { + harness: Harness.anthropic, + sequence: Sequence.orchestrator, + model: DEFAULT_AGENT_MODEL, + }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.anthropic, + }, + }), + input(), + ); + expect(result.outcome).toBe('failed'); + expect(result.failure).toBe(failure); + expect(harnessState.tasks).toHaveLength(1); + expect(fs.existsSync(path.join(tmp, QUEUE_DIR_NAME))).toBe(false); + }); + + it('returns an anthropic orchestrator task failure and cleans up the queue', async () => { + const failure = { + code: ErrorCodes.AgentAbort, + message: 'Authentication failed (401)', + }; + harnessState.taskFailure = failure; + const result = await runAgent( + config({ + binding: { + harness: Harness.anthropic, + sequence: Sequence.orchestrator, + model: DEFAULT_AGENT_MODEL, + }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.anthropic, + }, + }), + input(), + ); + expect(result.outcome).toBe('failed'); + expect(result.failure).toBe(failure); + expect(harnessState.tasks).toHaveLength(2); + expect(fs.existsSync(path.join(tmp, QUEUE_DIR_NAME))).toBe(false); + }); + + it.each([Sequence.linear, Sequence.orchestrator])( + 'preserves %s completion, shutdown and scan-flush ordering', + async (sequence) => { + const order: string[] = []; + vi.mocked(analytics.shutdown).mockImplementationOnce(() => { + order.push('shutdown'); + return Promise.resolve(); + }); + vi.mocked(flushScanReport).mockImplementationOnce(() => { + order.push('scan-flush'); + }); + const result = await runAgent( + config({ + binding: { + harness: Harness.pi, + sequence, + model: DEFAULT_AGENT_MODEL, + }, + }), + input(), + { + onProgress: (event) => { + if (event.kind === 'completion') { + order.push( + fs.existsSync(path.join(tmp, QUEUE_DIR_NAME)) + ? 'queue-present' + : 'queue-clean', + ); + order.push('completion'); + } + if (event.kind === 'lifecycle' && event.phase === 'completed') + order.push('outro'); + }, + }, + ); + expect(result.outcome).toBe('success'); + expect(order).toEqual([ + 'queue-clean', + 'completion', + 'outro', + 'shutdown', + 'scan-flush', + ]); + }, + ); + it('runs to success with an observer and an answerer', async () => { const events: AgentProgress[] = []; const ask = vi.fn(() => Promise.resolve({ q1: 'yes' })); @@ -217,6 +426,9 @@ describe('runAgent standalone', () => { // The agent's own snapshot, independent of the observer. expect(result.snapshot.dashboardUrl).toBe('https://d/1'); + expect(result.snapshot.notebookUrl).toBe('https://n/1'); + expect(result.snapshot.stage).toBe('Configure'); + expect(result.snapshot.finalCostUsd).toBe(0.25); expect(result.snapshot.tasks).toEqual([ { content: 'Install', status: 'completed' }, ]); @@ -227,7 +439,7 @@ describe('runAgent standalone', () => { cacheReadTokens: 0, cacheCreationTokens: 0, }); - expect(analytics.shutdown).toHaveBeenCalledWith('success'); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); }); it('runs to a complete result with no options at all', async () => { @@ -373,16 +585,4 @@ describe('runAgent standalone', () => { // What was reported before the crash survives in the snapshot. expect(result.snapshot.statusMessages).toContain('Installing the SDK'); }); - - it('is cancelled by a signal that is already aborted', async () => { - const controller = new AbortController(); - controller.abort(); - - const result = await runAgent(config(), input(), { - signal: controller.signal, - }); - - expect(result.outcome).toBe('cancelled'); - expect(harnessState.lastInputs).toBeUndefined(); - }); }); diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts index 3adf72fd9..2850afcc1 100644 --- a/src/lib/agent/agent-runner.ts +++ b/src/lib/agent/agent-runner.ts @@ -1,7 +1,7 @@ /** * Re-export shim. The runner has been split into agent/runner/. * Import from there directly; this shim keeps existing importers working. - * The session-driven `runAgent(programConfig, session)` lives in + * The session-driven `runProgramAgent(programConfig, session)` lives in * `src/lib/programs/run-agent-legacy.ts`. */ diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index a85820dd3..8be0d52f2 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -81,20 +81,13 @@ export type ProgressEmitter = (event: AgentProgress) => void; export interface AgentInteraction { /** * Open a question and resolve with the answers. The bridge that calls this - * owns the timeout, the `__cancelled__` sentinel and the analytics; `signal` - * aborts when the run is cancelled so the host can dismiss its overlay. + * owns the timeout, the `__cancelled__` sentinel and the analytics. */ - ask?: ( - question: PendingQuestion, - context: { signal: AbortSignal }, - ) => Promise; + ask?: (question: PendingQuestion) => Promise; /** Dismiss the in-flight question as cancelled (timeouts call this). */ cancelAsk?: () => void; /** Offer an optional step and resolve with whether to keep it. */ - taskNotice?: ( - notice: TaskNotice, - context: { signal: AbortSignal }, - ) => Promise; + taskNotice?: (notice: TaskNotice) => Promise; /** Dismiss an in-flight task notice as declined (timeouts call this). */ cancelTaskNotice?: () => void; } diff --git a/src/lib/agent/runner/README.md b/src/lib/agent/runner/README.md index a96dfaf12..5f43656cb 100644 --- a/src/lib/agent/runner/README.md +++ b/src/lib/agent/runner/README.md @@ -40,11 +40,11 @@ for the coordinated change checklist. Five layers, each with its own job. Nothing crosses layers unless it has to. **The entry point** (`index.ts`) is the front door: -`runAgent(config, input, {onProgress?, interaction?, signal?}) → RunResult`. It -takes resolved execution data and an invocation snapshot (`shared/types.ts`), -reports through `onProgress` and asks through `interaction` (`../progress.ts`), -and returns every ending as a result. It never renders, reads a session or -exits. The gates, OAuth, flags and binding lookup that used to run here live in +`runAgent(config, input, {onProgress?, interaction?}) → RunResult`. It takes +resolved execution data and an invocation snapshot (`shared/types.ts`), reports +through `onProgress` and asks through `interaction` (`../progress.ts`), and +returns every ending as a result. It never renders, reads a session or exits. +The gates, OAuth, flags and binding lookup that used to run here live in `src/lib/programs/run-agent-legacy.ts`, which also maps progress back onto `getUI()` for today's runners. diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index 6c60112aa..10ea0bdf8 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -5,7 +5,7 @@ * pipeline to a decided result. `config` is resolved execution data (which * program, how it is routed, which flags apply); `input` is the invocation * snapshot (directory, credentials, project); `options` carries an optional - * progress observer, an optional answerer and an optional cancel signal. + * progress observer, an optional answerer. * * The pipeline prepares the run (logging targets, gateway mint, scan triage), * then forks. The `orchestrator` variant routes to the task-queue runner. @@ -16,13 +16,15 @@ * exits the process and never rejects. A decided failure comes back in * `RunResult.failure` with the same fields `wizardAbort` takes; an error the * agent did not decide (a refused mint, an SDK crash) comes back as - * `outcome: 'crashed'` with the original error attached, so a caller can keep + * `outcome: RunOutcome.Crashed` with the original error attached, so a caller can keep * handling it the way it always did. The legacy adapter in * `src/lib/programs/run-agent-legacy.ts` rebuilds today's session-driven * behavior on top of this call for every existing caller. */ import { Sequence } from '@lib/constants'; +import { RunOutcome } from './shared/types'; +export { RunOutcome } from './shared/types'; import { classifyRunFailure } from '@lib/errors'; import { logToFile } from '@utils/debug'; import type { @@ -49,7 +51,6 @@ export type { RunFlags, RunHooks, RunInput, - RunOutcome, RunResult, RunSnapshot, SeedTaskEntry, @@ -77,7 +78,6 @@ export async function runAgent( ): Promise { const collector = createProgressCollector(options.onProgress); const { emit } = collector; - const signal = options.signal ?? new AbortController().signal; const log = (message: string) => emit({ kind: 'log', level: 'info', message }); @@ -101,7 +101,6 @@ export async function runAgent( boot, emit, interaction: options.interaction, - signal, }); return { ...result, @@ -114,7 +113,7 @@ export async function runAgent( const failure = classifyRunFailure(error); logToFile('[agent-runner] run crashed:', error); return { - outcome: 'crashed', + outcome: RunOutcome.Crashed, skillId: input.skillId, failure: { code: failure.code, diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index 38aed52f1..cbdc169dc 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -20,56 +20,25 @@ import { analytics } from '../../../../utils/analytics'; import { formatYaraAbortMessage } from '../../../yara-hooks'; import { installSkillById } from '../../../wizard-tools'; import { assemblePrompt } from '../../agent-prompt'; -import type { - AgentFailure, - RunResult, - RunSnapshot, - SequenceContext, -} from '../shared/types'; -import { installFailure } from '../shared/errors'; +import type { SequenceResult, SequenceContext } from '../shared/types'; +import { failed, installFailure } from '../shared/errors'; +import { RunOutcome } from '../shared/types'; import { shouldDisableAsk, runOptions } from '../shared/bootstrap'; import { createEmitSpinner } from '../shared/progress-collector'; import { createAskBridge } from '../shared/ask'; import { getHarness } from '../switchboard'; -/** The snapshot the sequence returns; `runAgent` fills it from its collector. */ -const PENDING_SNAPSHOT: RunSnapshot = { - tasks: [], - statusMessages: [], - usage: { - inputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheCreationTokens: 0, - }, -}; - -const failed = (failure: AgentFailure): RunResult => ({ - outcome: 'failed', - failure, - snapshot: PENDING_SNAPSHOT, -}); - -const cancelled = (): RunResult => ({ - outcome: 'cancelled', - failure: { message: 'Wizard setup cancelled.' }, - snapshot: PENDING_SNAPSHOT, -}); - export async function runLinearProgram({ config, input, boot, emit, interaction, - signal, -}: SequenceContext): Promise { +}: SequenceContext): Promise { const { run, composed } = config; const { skillsBaseUrl, credentials, project } = boot; const { projectApiKey, host, projectId } = credentials; - if (signal.aborted) return cancelled(); - // 5. Skill install (if skillId provided) let skillPath: string | undefined; if (run.skillId) { @@ -87,8 +56,6 @@ export async function runLinearProgram({ logToFile(`[agent-runner] skill installed at ${skillPath}`); } - if (signal.aborted) return cancelled(); - // 6. Initialize agent const spinner = createEmitSpinner(emit); @@ -102,7 +69,7 @@ export async function runLinearProgram({ shouldDisableAsk(input.flags) && process.env.WIZARD_ASK_AUTODRIVE !== '1'; const ask = askDisabled ? undefined - : createAskBridge(interaction, signal, { + : createAskBridge(interaction, { getSource: () => input.skillId ?? run.integrationLabel, richLinks: run.richLinks ?? false, timeoutMs: run.askTimeoutMs, @@ -180,7 +147,7 @@ export async function runLinearProgram({ matched: matched?.message ?? null, }); return { - outcome: 'aborted', + outcome: RunOutcome.Aborted, failure: { outroData, code: abortCode, @@ -194,7 +161,6 @@ export async function runLinearProgram({ abortCode, ), }, - snapshot: PENDING_SNAPSHOT, }; } @@ -325,10 +291,9 @@ export async function runLinearProgram({ await config.hooks.postRun(credentials); } - // A composed sub-run (integration inside self-driving) skips the terminal - // outro + analytics shutdown so the shared client survives the host's run. + // A composed sub-run leaves the terminal outro to its host. if (composed) { - return { outcome: 'success', snapshot: PENDING_SNAPSHOT }; + return { outcome: RunOutcome.Success }; } // 11. Outro @@ -349,8 +314,6 @@ export async function runLinearProgram({ emit({ kind: 'lifecycle', phase: 'completed', message: run.successMessage }); - // 12. Analytics shutdown await analytics.shutdown('success'); - - return { outcome: 'success', outro: outroData, snapshot: PENDING_SNAPSHOT }; + return { outcome: RunOutcome.Success, outro: outroData }; } diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts index f83794ad8..c76f75e86 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts @@ -9,6 +9,7 @@ import { } from '@lib/agent/runner/sequence/orchestrator/queue'; import { drainQueue, + RunTaskFatal, type RunTask, } from '@lib/agent/runner/sequence/orchestrator/executor'; @@ -39,6 +40,49 @@ describe('drainQueue', () => { return Promise.resolve(); }; + it('waits for live siblings after a fatal error and starts no dependents', async () => { + const fatal = new RunTaskFatal({ message: 'Authentication failed' }); + let release!: () => void; + const blocked = new Promise((resolve) => { + release = resolve; + }); + q.enqueue({ type: 'fatal' }); + const sibling = q.enqueue({ type: 'sibling' }); + q.enqueue({ type: 'dependent', dependsOn: [sibling.id] }); + const started: string[] = []; + const drain = drainQueue(q, async (task) => { + started.push(task.type); + if (task.type === 'fatal') throw fatal; + await blocked; + q.complete(task.id, HANDOFF); + }); + let settled = false; + const result = drain.catch((error: unknown) => { + settled = true; + return error; + }); + await new Promise((resolve) => setImmediate(resolve)); + expect(settled).toBe(false); + expect(started).toEqual(['fatal', 'sibling']); + release(); + expect(await result).toBe(fatal); + expect(q.get(sibling.id)?.status).toBe(TaskStatus.Done); + expect(started).toEqual(['fatal', 'sibling']); + }); + + it('preserves a fatal failure when a sibling completes in the same turn', async () => { + q.enqueue({ type: 'success' }); + q.enqueue({ type: 'fatal' }); + const fatal = new RunTaskFatal({ message: 'Authentication failed' }); + await expect( + drainQueue(q, (task) => { + if (task.type === 'fatal') return Promise.reject(fatal); + q.complete(task.id, HANDOFF); + return Promise.resolve(); + }), + ).rejects.toBe(fatal); + }); + it('runs a single task to done and drains', async () => { const a = q.enqueue({ type: 'install' }); await drainQueue(q, completing, { maxStarts: 50 }); diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts index 40c6642f8..79056bd33 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts @@ -70,7 +70,7 @@ describe('task notice timeout', () => { // Nobody presses anything. showTaskNotice.mockReturnValue(new Promise(() => undefined)); - const promise = offerSeededTask(NOTICE, 1000, interaction); + const promise = offerSeededTask(NOTICE, { timeoutMs: 1000, interaction }); vi.advanceTimersByTime(1000); await expect(promise).resolves.toEqual({ keep: false, timedOut: true }); @@ -86,7 +86,10 @@ describe('task notice timeout', () => { try { showTaskNotice.mockResolvedValue(true); - const result = await offerSeededTask(NOTICE, 1000, interaction); + const result = await offerSeededTask(NOTICE, { + timeoutMs: 1000, + interaction, + }); expect(result).toEqual({ keep: true, timedOut: false }); vi.advanceTimersByTime(5000); @@ -105,12 +108,12 @@ describe('task notice timeout', () => { // Both decline, but only one of them means "the user was not there" — // the run reports them differently, and now skips them differently too. - await expect(offerSeededTask(NOTICE, 1000, interaction)).resolves.toEqual( - { - keep: false, - timedOut: false, - }, - ); + await expect( + offerSeededTask(NOTICE, { timeoutMs: 1000, interaction }), + ).resolves.toEqual({ + keep: false, + timedOut: false, + }); expect(cancelTaskNotice).not.toHaveBeenCalled(); } finally { vi.useRealTimers(); @@ -170,7 +173,7 @@ describe('askSeededConsent', () => { showTaskNotice.mockResolvedValue(true); await expect( - askSeededConsent('warehouse', NOTICE, 1000, interaction), + askSeededConsent('warehouse', NOTICE, { timeoutMs: 1000, interaction }), ).resolves.toEqual({ keep: true, timedOut: false, @@ -182,7 +185,7 @@ describe('askSeededConsent', () => { showTaskNotice.mockResolvedValue(false); await expect( - askSeededConsent('warehouse', NOTICE, 1000, interaction), + askSeededConsent('warehouse', NOTICE, { timeoutMs: 1000, interaction }), ).resolves.toEqual({ keep: false, timedOut: false, @@ -195,7 +198,10 @@ describe('askSeededConsent', () => { try { showTaskNotice.mockReturnValue(new Promise(() => undefined)); - const promise = askSeededConsent('warehouse', NOTICE, 1000, interaction); + const promise = askSeededConsent('warehouse', NOTICE, { + timeoutMs: 1000, + interaction, + }); vi.advanceTimersByTime(1000); await expect(promise).resolves.toEqual({ @@ -211,7 +217,10 @@ describe('askSeededConsent', () => { it('reports the answer on one event per offer', async () => { showTaskNotice.mockResolvedValue(true); - await askSeededConsent('warehouse', NOTICE, 1000, interaction); + await askSeededConsent('warehouse', NOTICE, { + timeoutMs: 1000, + interaction, + }); expect(wizardCapture).toHaveBeenCalledTimes(1); expect(wizardCapture).toHaveBeenCalledWith( @@ -226,7 +235,7 @@ describe('askSeededConsent', () => { showTaskNotice.mockRejectedValue(new Error('UI blew up')); await expect( - askSeededConsent('warehouse', NOTICE, 1000, interaction), + askSeededConsent('warehouse', NOTICE, { timeoutMs: 1000, interaction }), ).resolves.toEqual({ keep: false, timedOut: false, @@ -259,7 +268,10 @@ describe('offering notices from the seed loop', () => { []; for (const entry of entries) { answers.push( - await askSeededConsent(entry.type, NOTICE, 60_000, interaction), + await askSeededConsent(entry.type, NOTICE, { + timeoutMs: 60_000, + interaction, + }), ); } return answers; diff --git a/src/lib/agent/runner/sequence/orchestrator/executor.ts b/src/lib/agent/runner/sequence/orchestrator/executor.ts index e17e8c269..e70e5723c 100644 --- a/src/lib/agent/runner/sequence/orchestrator/executor.ts +++ b/src/lib/agent/runner/sequence/orchestrator/executor.ts @@ -112,19 +112,25 @@ export async function drainQueue( ): Promise { const running = new Map>(); let starts = 0; + let failure: { error: unknown } | undefined; - for (;;) { - for (const task of store.nextRunnable()) { - if (++starts > opts.maxStarts) break; - // runOne marks the task running synchronously, so the next - // nextRunnable() call no longer offers it. - const p = runOne(store, runTask, task).finally(() => - running.delete(task.id), - ); - running.set(task.id, p); + try { + for (;;) { + if (failure) throw failure.error; + for (const task of store.nextRunnable()) { + if (++starts > opts.maxStarts) break; + const p = runOne(store, runTask, task) + .catch((error: unknown) => { + failure ??= { error }; + }) + .finally(() => running.delete(task.id)); + running.set(task.id, p); + } + if (running.size === 0) break; + await Promise.race(running.values()); } - if (running.size === 0) break; - // Wake on the first finish; it may have unblocked dependents or requeued. - await Promise.race(running.values()); + } finally { + // No queue or skill cleanup may run while a sibling still uses them. + await Promise.allSettled(running.values()); } } diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index da9b0d4e2..43c41c137 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -10,6 +10,8 @@ * task resolve to a prompt fetched at startup into the registry. The wizard side * stays product-ignorant: it is the queue, the executor, and the loader. */ +import { failed } from '../../shared/errors'; +import { RunOutcome } from '../../shared/types'; import { randomUUID } from 'crypto'; import { cpSync, @@ -36,8 +38,7 @@ import { ErrorCodes } from '@lib/errors'; import type { AgentInteraction } from '@lib/agent/progress'; import type { AgentFailure, - RunResult, - RunSnapshot, + SequenceResult, SequenceContext, } from '../../shared/types'; import { createEmitSpinner } from '../../shared/progress-collector'; @@ -205,6 +206,11 @@ function resolveReferenceSkillId( */ export const TASK_NOTICE_TIMEOUT_MS = 5 * 60 * 1000; +interface SeededTaskOptions { + timeoutMs?: number; + interaction?: AgentInteraction; +} + /** * Offer an optional step, defaulting to declining it if nobody answers. * @@ -215,9 +221,7 @@ export const TASK_NOTICE_TIMEOUT_MS = 5 * 60 * 1000; */ export async function offerSeededTask( notice: TaskNotice, - timeoutMs: number = TASK_NOTICE_TIMEOUT_MS, - interaction?: AgentInteraction, - signal: AbortSignal = new AbortController().signal, + { timeoutMs = TASK_NOTICE_TIMEOUT_MS, interaction }: SeededTaskOptions = {}, ): Promise<{ keep: boolean; timedOut: boolean }> { // No one to show the notice to: a step nobody can answer for must not run. // The same answer a non-interactive host gives today. @@ -236,7 +240,7 @@ export async function offerSeededTask( }, timeoutMs); }); try { - const keep = await Promise.race([taskNotice(notice, { signal }), timeout]); + const keep = await Promise.race([taskNotice(notice), timeout]); return { keep, timedOut }; } finally { if (timer) clearTimeout(timer); @@ -337,16 +341,9 @@ export function skipDeclinedSeededTasks( export async function askSeededConsent( type: string, notice: TaskNotice, - timeoutMs?: number, - interaction?: AgentInteraction, - signal?: AbortSignal, + options: SeededTaskOptions = {}, ): Promise { - const consent = await offerSeededTask( - notice, - timeoutMs, - interaction, - signal, - ).then( + const consent = await offerSeededTask(notice, options).then( (answer): SeededConsent => ({ ...answer, errored: false }), (err: unknown): SeededConsent => { logToFile( @@ -435,44 +432,40 @@ export function displayOrder( .map((entry) => entry.task); } -/** The snapshot the sequence returns; `runAgent` fills it from its collector. */ -const PENDING_SNAPSHOT: RunSnapshot = { - tasks: [], - statusMessages: [], - usage: { - inputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheCreationTokens: 0, - }, -}; - -const failed = (failure: AgentFailure): RunResult => ({ - outcome: 'failed', - failure, - snapshot: PENDING_SNAPSHOT, -}); +export async function runOrchestrator( + context: SequenceContext, +): Promise { + let cleaned = false; + const cleanupQueue = (): void => { + if (cleaned) return; + cleaned = true; + try { + rmSync(path.join(context.input.installDir, QUEUE_DIR_NAME), { + recursive: true, + force: true, + }); + } catch (error) { + analytics.captureException( + error instanceof Error ? error : new Error(String(error)), + { step: 'orchestrator_cache_cleanup' }, + ); + } + }; + try { + return await executeOrchestrator(context, cleanupQueue); + } finally { + cleanupQueue(); + } +} -export async function runOrchestrator({ - config, - input, - boot, - emit, - interaction, - signal, -}: SequenceContext): Promise { +async function executeOrchestrator( + { config, input, boot, emit, interaction }: SequenceContext, + cleanupQueue: () => void, +): Promise { const runId = randomUUID(); const { run } = config; const programId = config.programId; - if (signal.aborted) { - return { - outcome: 'cancelled', - failure: { message: 'Wizard setup cancelled.' }, - snapshot: PENDING_SNAPSHOT, - }; - } - // Switchboard context — reused for every per-role harness resolution below. // The caller resolved the run-level binding from it; per-task roles overlay // `binding.contextMillOverride[role]` on the same inputs. @@ -790,13 +783,7 @@ export async function runOrchestrator({ // not, which is why the offer lives here and not there. seededConsent.set( task.id, - await askSeededConsent( - seeded.type, - seeded.notice, - undefined, - interaction, - signal, - ), + await askSeededConsent(seeded.type, seeded.notice, { interaction }), ); } logToFile(`[orchestrator] runner-seeded task ${seeded.type}`); @@ -830,7 +817,7 @@ export async function runOrchestrator({ // Absent in CI and signup, where nobody can answer. const askBridge = shouldDisableAsk(input.flags) ? undefined - : createAskBridge(interaction, signal, { + : createAskBridge(interaction, { getSource: () => input.skillId ?? programId, beforeShow: () => { // How late the first ask lands is the measure of this run shape: it @@ -878,8 +865,7 @@ export async function runOrchestrator({ requestRemark: false, analyticsProperties: { task_type: 'seed', harness: seedPick.harness }, }); - // A decided failure (a 401 the harness already reported) ends the run here, - // before anything is queued, exactly where the harness used to exit. + // A decided seed failure ends the run and releases its queue artifacts. if (seedResult.failure) return failed(seedResult.failure); if (seedResult.error) { logToFile( @@ -1140,20 +1126,7 @@ export async function runOrchestrator({ { step: 'orchestrator_reference_promote' }, ); } - // Success or failure, no run artifact outlives the run — wipe the whole - // cache folder (queue, handoffs, reference example, installed task - // instructions). The .DELETE-ME.md inside is the fallback if we don't. - try { - rmSync(path.join(input.installDir, QUEUE_DIR_NAME), { - recursive: true, - force: true, - }); - } catch (err) { - analytics.captureException( - err instanceof Error ? err : new Error(String(err)), - { step: 'orchestrator_cache_cleanup' }, - ); - } + cleanupQueue(); try { sweepRunInstalledSkills( claudeSkillsDir, @@ -1285,5 +1258,5 @@ export async function runOrchestrator({ emit({ kind: 'completion', outro }); emit({ kind: 'lifecycle', phase: 'completed', message }); await analytics.shutdown('success'); - return { outcome: 'success', outro, snapshot: PENDING_SNAPSHOT }; + return { outcome: RunOutcome.Success, outro }; } diff --git a/src/lib/agent/runner/shared/ask.ts b/src/lib/agent/runner/shared/ask.ts index b109f053b..b7c6c1050 100644 --- a/src/lib/agent/runner/shared/ask.ts +++ b/src/lib/agent/runner/shared/ask.ts @@ -17,7 +17,6 @@ import type { AgentInteraction } from '@lib/agent/progress'; export function createAskBridge( interaction: AgentInteraction | undefined, - signal: AbortSignal, options: { getSource: () => string; richLinks: boolean; @@ -33,7 +32,7 @@ export function createAskBridge( getSource: options.getSource, showQuestion: (question) => { options.beforeShow?.(); - return ask(question, { signal }); + return ask(question); }, cancelQuestion: interaction?.cancelAsk, richLinks: options.richLinks, diff --git a/src/lib/agent/runner/shared/bootstrap.ts b/src/lib/agent/runner/shared/bootstrap.ts index 1b776a87c..7384c5f1a 100644 --- a/src/lib/agent/runner/shared/bootstrap.ts +++ b/src/lib/agent/runner/shared/bootstrap.ts @@ -8,10 +8,9 @@ * arrives already resolved in `RunConfig` and `RunInput`. */ -import { analytics } from '@utils/analytics'; import { createTriageLLMProvider } from '@lib/agent/triage-provider'; import { gatewayAuth } from '@lib/gateway-session'; -import { enableDebugLogs, logToFile, initLogFile } from '@utils/debug'; +import { logToFile } from '@utils/debug'; import { CallType, IS_DEV } from '@lib/constants'; import { VERSION } from '@lib/version'; import { mcpUrlFor } from '@lib/host-resolution'; @@ -67,19 +66,6 @@ export async function prepareRun( config: RunConfig, input: RunInput, ): Promise { - const { run } = config; - - // 1. Init logging + debug - initLogFile(); - logToFile( - `[agent-runner] START ${run.integrationLabel} build=${analytics.build}` + - `${input.flags.ci ? ' (non-interactive)' : ''}`, - ); - - if (input.flags.debug) { - enableDebugLogs(); - } - const { skillsBaseUrl } = config; // Where this run actually points. The three services switch independently, diff --git a/src/lib/agent/runner/shared/errors.ts b/src/lib/agent/runner/shared/errors.ts index c012b59a8..8578d2a0c 100644 --- a/src/lib/agent/runner/shared/errors.ts +++ b/src/lib/agent/runner/shared/errors.ts @@ -5,7 +5,12 @@ import type { InstallSkillResult } from '@lib/wizard-tools'; import { skillErrorCode } from '@lib/errors'; import { WizardError } from '@utils/wizard-abort'; -import type { AgentFailure } from './types'; +import { RunOutcome, type AgentFailure, type SequenceResult } from './types'; + +export const failed = (failure: AgentFailure): SequenceResult => ({ + outcome: RunOutcome.Failed, + failure, +}); /** The failure a skill install error decides. The caller reports and exits. */ export function installFailure( diff --git a/src/lib/agent/runner/shared/progress-collector.ts b/src/lib/agent/runner/shared/progress-collector.ts index ee10ff7d8..6f493dbc3 100644 --- a/src/lib/agent/runner/shared/progress-collector.ts +++ b/src/lib/agent/runner/shared/progress-collector.ts @@ -9,6 +9,7 @@ */ import { logToFile } from '@utils/debug'; +import { appendStatus } from '@lib/status-history'; import type { AgentProgress, ProgressEmitter, @@ -41,7 +42,10 @@ export function createProgressCollector( snapshot.tasks = event.tasks.map((t) => ({ ...t })); break; case 'status': - snapshot.statusMessages.push(event.message); + snapshot.statusMessages = appendStatus( + snapshot.statusMessages, + event.message, + ); break; case 'stage': snapshot.stage = event.stage; @@ -68,7 +72,7 @@ export function createProgressCollector( apply(event); if (!onProgress) return; try { - onProgress(event); + onProgress(structuredClone(event)); } catch (error) { // A broken projection is the host's problem, not the run's. Say so in // the log and carry on; the snapshot above is the source of truth. diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index 388695020..20961435b 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -181,7 +181,7 @@ export interface RunConfig { programId: string; /** The program's run definition. Its session-taking hooks are the caller's, see `hooks`. */ run: ProgramRun; - /** A composed sub-run skips the terminal outro and the analytics shutdown. */ + /** A composed sub-run leaves the terminal outro to its host. */ composed: boolean; /** Run-level sequence, harness and model. */ binding: ResolvedBinding; @@ -289,21 +289,12 @@ export interface AgentFailure { detail?: Record; } -export type RunOutcome = - /** The run finished; `outro` holds what to show. */ - | 'success' - /** The agent chose to stop (`[ABORT]`); `failure` holds the rendered case. */ - | 'aborted' - /** A coded failure the agent decided; `failure` holds it. */ - | 'failed' - /** `options.signal` aborted the run. */ - | 'cancelled' - /** - * Something threw that the agent did not decide (a refused gateway mint, an - * SDK crash). `failure.error` is the original error and `failure.code` its - * classification, so a caller can report it or rethrow it as it did before. - */ - | 'crashed'; +export enum RunOutcome { + Success = 'success', + Aborted = 'aborted', + Failed = 'failed', + Crashed = 'crashed', +} /** Totals of every `usage` event the run emitted. */ export interface TokenUsageTotals { @@ -324,28 +315,34 @@ export interface RunSnapshot { notebookUrl?: string; } -/** - * `runAgent` never rejects: every ending is one of these, and every ending - * that is not `success` carries a `failure` the caller can act on. - */ -export interface RunResult { - outcome: RunOutcome; +/** A sequence decides an outcome; the dispatcher owns its snapshot. */ +export type SequenceResult = + | { outcome: RunOutcome.Success; outro?: OutroData; failure?: never } + | { + outcome: RunOutcome.Aborted | RunOutcome.Failed; + failure: AgentFailure; + outro?: never; + }; + +/** Every non-success result carries a failure; crashes preserve the original error. */ +export type RunResult = ( + | SequenceResult + | { + outcome: RunOutcome.Crashed; + failure: AgentFailure & { error: Error }; + outro?: never; + } +) & { /** The skill this run installed or was for. */ skillId?: string; - /** Success outro. Absent for a composed sub-run, which has no terminal outro. */ - outro?: OutroData; - /** Present whenever `outcome` is not `success`. */ - failure?: AgentFailure; snapshot: RunSnapshot; -} +}; export interface RunAgentOptions { /** Receives every progress event in emission order. Never awaited. */ onProgress?: (event: import('@lib/agent/progress').AgentProgress) => void; /** Answers the agent's questions. Absent → no ask bridge, notices declined. */ interaction?: AgentInteraction; - /** Cancels the run at its next phase boundary and any pending question. */ - signal?: AbortSignal; } /** What a sequence receives: the contracts plus the prepared run. */ @@ -355,5 +352,4 @@ export interface SequenceContext { boot: BootstrapResult; emit: ProgressEmitter; interaction: AgentInteraction | undefined; - signal: AbortSignal; } diff --git a/src/lib/agent/runner/switchboard/sequence.ts b/src/lib/agent/runner/switchboard/sequence.ts index 521eda57b..3d2730405 100644 --- a/src/lib/agent/runner/switchboard/sequence.ts +++ b/src/lib/agent/runner/switchboard/sequence.ts @@ -12,7 +12,7 @@ import { resolveFlagSequence, } from './flags'; import { getHarness, resolveHarness } from './harness'; -import type { RunResult, SequenceContext } from '../shared/types'; +import type { SequenceResult, SequenceContext } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; import { @@ -28,7 +28,7 @@ import { export interface SequenceRunner { readonly name: Sequence; /** Run one program to a decided result. Unexpected errors propagate. */ - run(ctx: SequenceContext): Promise; + run(ctx: SequenceContext): Promise; } export const SEQUENCE_OPTIONS: Partial> = { diff --git a/src/lib/detection/__tests__/agentic-progress.test.ts b/src/lib/detection/__tests__/agentic-progress.test.ts new file mode 100644 index 000000000..387bb24fc --- /dev/null +++ b/src/lib/detection/__tests__/agentic-progress.test.ts @@ -0,0 +1,80 @@ +import { detectProjectsWithAgent } from '../agentic'; +import { + initializeAgent, + runAgent as executeAgent, +} from '@lib/agent/agent-interface'; +import { buildSession } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; +import { getUI } from '@ui'; + +vi.mock('@utils/debug'); +vi.mock('@ui', () => ({ getUI: () => ui })); +const ui = vi.hoisted(() => ({ + addTokenUsage: vi.fn(), + setStage: vi.fn(), + pushStatus: vi.fn(), + log: { error: vi.fn() }, +})); +vi.mock('@lib/agent/agent-interface', async (original) => ({ + ...(await original()), + initializeAgent: vi.fn(), + runAgent: vi.fn(), +})); + +it('keeps initialization and execution progress visible during detection', async () => { + const delta = { + inputTokens: 5, + outputTokens: 2, + cacheReadTokens: 0, + cacheCreationTokens: 0, + cacheCreation5m: 0, + cacheCreation1h: 0, + }; + vi.mocked(initializeAgent).mockImplementation((config) => { + config.emit?.({ + kind: 'log', + level: 'error', + message: 'Initialization diagnostic', + }); + return Promise.resolve({ emit: config.emit } as Awaited< + ReturnType + >); + }); + vi.mocked(executeAgent).mockImplementation( + (config, _prompt, _options, _spinner, _messages, middleware) => { + config.emit?.({ kind: 'usage', delta }); + config.emit?.({ kind: 'stage', stage: 'Scanning' }); + config.emit?.({ kind: 'status', message: 'Found a project' }); + config.emit?.({ + kind: 'log', + level: 'error', + message: 'Execution diagnostic', + }); + middleware?.onMessage({ + type: 'result', + result: + '{"projects":[{"path":".","targetId":"node","framework":"Node.js"}]}', + }); + return Promise.resolve({}); + }, + ); + const session = buildSession({ installDir: '/tmp/detection-test' }); + session.credentials = { + accessToken: 'test', + projectApiKey: 'phc_test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }; + const report = await detectProjectsWithAgent(session, { + programId: 'posthog-integration', + targets: [{ id: 'node', name: 'Node.js' }], + }); + expect(report.projects[0].targetId).toBe('node'); + expect(getUI().addTokenUsage).toHaveBeenCalledWith(delta); + expect(ui.setStage).toHaveBeenCalledWith('Scanning'); + expect(ui.pushStatus).toHaveBeenCalledWith('Found a project'); + expect(ui.log.error.mock.calls).toEqual([ + ['Initialization diagnostic'], + ['Execution diagnostic'], + ]); +}); diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index aac8d282a..c4106e99c 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -25,7 +25,8 @@ import { CallType, getSkillsBaseUrl, HAIKU_MODEL } from '@lib/constants'; import { analytics } from '@utils/analytics'; import type { WizardSession } from '@lib/wizard-session'; import type { WizardRunOptions } from '@utils/types'; -import type { SpinnerHandle } from '@ui'; +import { getUI, type SpinnerHandle } from '@ui'; +import { createUiReducer } from '@ui/agent-progress'; /** A category the agent classifies each project into (id the agent returns). */ export type DetectTarget = { id: string; name: string }; @@ -360,6 +361,7 @@ export async function detectProjectsWithAgent( const agent = await initializeAgent( { + emit: createUiReducer(getUI()), workingDirectory: cwd, posthogMcpUrl: host.mcpUrl, posthogApiKey: accessToken, diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts new file mode 100644 index 000000000..6a0b9b1c4 --- /dev/null +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -0,0 +1,227 @@ +import { runNonInteractive } from '@lib/runners/run-non-interactive'; +import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { runProgramAgent } from '../run-agent-legacy'; +import { runAgent, RunOutcome, type RunResult } from '@lib/agent/runner'; +import { Harness, Sequence } from '@lib/constants'; +import { buildSession, OutroKind } from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; +import { LoggingUI } from '@ui/logging-ui'; +import { setUI } from '@ui'; +import { analytics } from '@utils/analytics'; +import { initLogFile } from '@utils/debug'; +import { wizardAbort } from '@utils/wizard-abort'; +import type { ProgramConfig } from '../program-step'; + +const streamShutdown = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +vi.mock('@env', async (original) => ({ + ...(await original()), + IS_PRODUCTION_BUILD: false, +})); +vi.mock('@lib/local-dev', async (original) => ({ + ...(await original()), + checkLocalServices: vi.fn().mockResolvedValue(null), +})); +vi.mock('@utils/environment', async (original) => ({ + ...(await original()), + readEnvironment: () => ({}), +})); +vi.mock('@lib/gateway-session', async (original) => ({ + ...(await original()), + configureGatewayFromCIEnvironment: vi.fn(), +})); +vi.mock('@lib/task-stream/index', () => ({ + TaskStreamPush: class { + attach = vi.fn(); + shutdown = streamShutdown; + }, + PostHogDestination: class {}, + createFileDestination: () => null, +})); +vi.mock('@utils/debug'); +vi.mock('@utils/analytics', () => ({ + analytics: { + build: 'test', + runId: 'run-1', + wizardCapture: vi.fn(), + setTag: vi.fn(), + getAllFlagsForWizard: vi.fn().mockResolvedValue({}), + getWizardFlagPayloads: vi.fn().mockReturnValue({}), + shutdown: vi.fn().mockResolvedValue(undefined), + }, + sessionProperties: () => ({}), +})); +vi.mock('@lib/agent/runner', async (original) => ({ + ...(await original()), + runAgent: vi.fn(), +})); +vi.mock('@lib/agent/runner/shared/authenticate', () => ({ + authenticate: vi.fn().mockResolvedValue(undefined), + refreshAccessTokenIfNeeded: vi.fn().mockResolvedValue(undefined), +})); +vi.mock('@lib/agent/claude-settings', () => ({ + checkAllSettingsConflicts: vi.fn().mockReturnValue([]), + restoreClaudeSettings: vi.fn(), +})); +vi.mock('@utils/wizard-abort', async (original) => ({ + ...(await original()), + registerCleanup: vi.fn(), + wizardAbort: vi.fn().mockResolvedValue(undefined), +})); +vi.mock('../posthog-integration/detect', () => ({ + maybeStampAiSdkDetected: vi.fn(), +})); + +const program = (id: ProgramConfig['id'] = 'metrics'): ProgramConfig => ({ + id, + steps: [], + description: 'Test', + run: { + integrationLabel: 'test', + spinnerMessage: 'Working', + successMessage: 'Done', + estimatedDurationMinutes: 1, + reportFile: 'report.md', + docsUrl: 'https://docs.test', + }, +}); +const snapshot = { + tasks: [], + statusMessages: [], + usage: { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + }, +}; +const session = () => ({ + ...buildSession({ ci: true, installDir: '/tmp/adapter-test' }), + credentials: { + accessToken: 'test', + projectApiKey: 'phc_test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }, +}); + +let logSpy: ReturnType; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(authenticate).mockImplementation((sess) => { + sess.credentials = session().credentials; + return Promise.resolve(); + }); + setUI(new LoggingUI()); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + vi.mocked(runAgent).mockImplementation((_config, _input, options) => { + options?.onProgress?.({ kind: 'status', message: 'Working' }); + options?.onProgress?.({ + kind: 'completion', + outro: { kind: OutroKind.Success, message: 'Done' }, + }); + options?.onProgress?.({ + kind: 'lifecycle', + phase: 'completed', + message: 'Done', + }); + return Promise.resolve({ outcome: RunOutcome.Success, snapshot }); + }); +}); +afterEach(() => logSpy.mockRestore()); + +it.each([ + ['metrics', Harness.pi, Sequence.orchestrator], + ['replay-vision', Harness.anthropic, Sequence.orchestrator], +] as const)( + 'forwards the %s program binding through the real adapter', + async (id, harness, sequence) => { + await runProgramAgent(program(id), session()); + expect(runAgent).toHaveBeenCalledWith( + expect.objectContaining({ + programId: id, + binding: expect.objectContaining({ harness, sequence }), + }), + expect.objectContaining({ flags: expect.objectContaining({ ci: true }) }), + expect.objectContaining({ + onProgress: expect.any(Function), + interaction: expect.any(Object), + }), + ); + expect(logSpy).toHaveBeenCalledWith('◇ Working'); + expect(logSpy).toHaveBeenCalledWith('└ Done'); + expect(initLogFile).toHaveBeenCalledOnce(); + expect(analytics.shutdown).not.toHaveBeenCalled(); + }, +); + +it('clamps a composed program to linear and keeps host analytics alive', async () => { + await runProgramAgent(program(), session(), { composed: true }); + expect(runAgent).toHaveBeenCalledWith( + expect.objectContaining({ + composed: true, + binding: expect.objectContaining({ sequence: Sequence.linear }), + }), + expect.anything(), + expect.anything(), + ); + expect(analytics.shutdown).not.toHaveBeenCalled(); +}); + +it.each([RunOutcome.Aborted, RunOutcome.Failed] as const)( + 'passes a %s result to the existing abort handler', + async (outcome) => { + const failure = { message: 'Failed', exitCode: 2 }; + vi.mocked(runAgent).mockResolvedValue({ outcome, failure, snapshot }); + await runProgramAgent(program(), session()); + expect(wizardAbort).toHaveBeenCalledExactlyOnceWith(failure); + expect(analytics.shutdown).not.toHaveBeenCalled(); + }, +); + +it('rethrows the original crash for the outer runner', async () => { + const error = new Error('mint refused'); + const result: RunResult = { + outcome: RunOutcome.Crashed, + failure: { error }, + snapshot, + }; + vi.mocked(runAgent).mockResolvedValue(result); + await expect(runProgramAgent(program(), session())).rejects.toBe(error); + expect(wizardAbort).not.toHaveBeenCalled(); + expect(analytics.shutdown).not.toHaveBeenCalled(); +}); + +it.each([ + [Harness.pi, Sequence.linear], + [Harness.pi, Sequence.orchestrator], + [Harness.anthropic, Sequence.linear], + [Harness.anthropic, Sequence.orchestrator], +])( + 'runs headless %s/%s through the real CLI adapter', + async (harness, sequence) => { + runNonInteractive( + program(), + { + apiKey: 'phx_test', + projectId: '1', + installDir: '/tmp/adapter-test', + telemetry: false, + harness, + sequence, + }, + 'headless', + ); + await vi.waitFor(() => expect(streamShutdown).toHaveBeenCalledOnce()); + expect(wizardAbort).not.toHaveBeenCalled(); + expect(runAgent).toHaveBeenCalledWith( + expect.objectContaining({ + binding: expect.objectContaining({ harness, sequence }), + }), + expect.objectContaining({ flags: expect.objectContaining({ ci: true }) }), + expect.anything(), + ); + expect(logSpy).toHaveBeenCalledWith('◇ Working'); + expect(logSpy).toHaveBeenCalledWith('└ Done'); + }, +); diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index d495b5374..c7ea2560a 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; -import { runAgent } from '@lib/programs/run-agent-legacy'; +import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import type { ProgramRun } from '@lib/agent/agent-runner'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import type { WizardSession } from '@lib/wizard-session'; @@ -479,7 +479,7 @@ export const integrationRunStep: ProgramStep = { // 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 }), + runProgramAgent(posthogIntegrationConfig, session, { composed: true }), isComplete: (session) => session.runPhase === RunPhase.Completed || session.runPhase === RunPhase.Error, diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index 4d4c53612..d8315cb88 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -1,7 +1,7 @@ /** * The session-driven agent runner every existing caller uses. * - * `runAgent(programConfig, session)` rebuilds today's behavior on top of the + * `runProgramAgent(programConfig, session)` rebuilds today's behavior on top of the * functional `runAgent(config, input, options)` in `@lib/agent/runner`: it * runs the gates the TUI owns (health, settings, AI opt-in, post-auth steps), * authenticates, resolves the program's binding, builds the agent's inputs @@ -15,11 +15,11 @@ import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; -import { getUI, type WizardUI } from '@ui'; -import type { SpinnerHandle } from '@lib/agent/progress'; -import type { AgentInteraction, AgentProgress } from '@lib/agent/progress'; +import { getUI } from '@ui'; +import { createUiReducer, uiInteraction } from '@ui/agent-progress'; import { - runAgent as runAgentFunctional, + runAgent, + RunOutcome, resolveBinding, type ProgramRun, type RunConfig, @@ -66,7 +66,7 @@ import { startAuditLedgerWatcher } from './audit/ledger-watcher'; * Resolve a ProgramConfig's agent run definition and execute the pipeline. * Entry point for the runners and for composed run steps. */ -export async function runAgent( +export async function runProgramAgent( programConfig: ProgramConfig, session: WizardSession, options: { composed?: boolean } = {}, @@ -270,20 +270,16 @@ async function runProgram( }, }; - const result = await runAgentFunctional(config, input, { + const result = await runAgent(config, input, { onProgress: createUiReducer(ui), interaction: uiInteraction(ui), }); - // Success already rendered through the reducer (outro data, outro line) and - // shut analytics down. A decided failure exits exactly as it always has. An - // error the agent did not decide used to propagate out of this call, and the - // runners handle it (mint-failure screen, classifyRunFailure): keep throwing - // the original error for them. - if (result.outcome === 'crashed') { - throw result.failure?.error ?? new Error(result.failure?.message); + // The host owns process exits and rethrowing crashes. + if (result.outcome === RunOutcome.Crashed) { + throw result.failure.error; } - if (result.outcome !== 'success') { + if (result.outcome !== RunOutcome.Success) { await wizardAbort(result.failure); } } @@ -468,66 +464,3 @@ function captureSwitchboardDecision( ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, ); } - -// ── Progress → WizardUI, one call per event ─────────────────────────── - -/** - * The inverse of the agent's former `getUI()` calls: one event, one - * `WizardUI` method, synchronous, in emission order. Because each case maps - * back to exactly the call the agent used to make, the frame and flow goldens - * hold without regeneration. - */ -function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { - let spinner: SpinnerHandle | undefined; - return (event) => { - switch (event.kind) { - case 'lifecycle': - if (event.phase === 'started') ui.startRun(); - else ui.outro(event.message); - break; - case 'spinner': { - const handle = (spinner ??= ui.spinner()); - handle[event.action](event.message); - break; - } - case 'log': - ui.log[event.level](event.message); - break; - case 'status': - ui.pushStatus(event.message); - break; - case 'tasks': - ui.syncTodos(event.tasks); - break; - case 'stage': - ui.setStage(event.stage); - break; - case 'url': - if (event.which === 'dashboard') ui.setDashboardUrl(event.url); - else ui.setNotebookUrl(event.url); - break; - case 'usage': - ui.addTokenUsage(event.delta); - break; - case 'finalCost': - ui.setFinalTokenCostUsd(event.usd); - break; - case 'authError': - ui.showAuthError(event.detail); - break; - case 'completion': - ui.setOutroData(event.outro); - break; - } - }; -} - -/** The agent's questions, answered wherever `getUI()` answers them today. */ -function uiInteraction(ui: WizardUI): AgentInteraction { - return { - ask: (question) => ui.requestQuestion(question), - cancelAsk: () => ui.cancelPendingQuestion(), - taskNotice: (notice) => ui.showTaskNotice(notice), - cancelTaskNotice: () => ui.cancelTaskNotice(), - }; -} diff --git a/src/lib/runners/__tests__/mint-recovery.test.ts b/src/lib/runners/__tests__/mint-recovery.test.ts index 076bf9a20..9a564640e 100644 --- a/src/lib/runners/__tests__/mint-recovery.test.ts +++ b/src/lib/runners/__tests__/mint-recovery.test.ts @@ -1,6 +1,6 @@ import { vi, it, expect, afterEach } from 'vitest'; import { runWizard } from '../run-wizard'; -import { runAgent } from '@lib/programs/run-agent-legacy'; +import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { startTUI } from '@ui/tui/start-tui'; import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; @@ -10,7 +10,7 @@ import { ScreenId } from '@ui/tui/router'; import { HostResolution } from '@lib/host-resolution'; import { analytics } from '@utils/analytics'; -vi.mock('@lib/programs/run-agent-legacy', () => ({ runAgent: vi.fn() })); +vi.mock('@lib/programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn() })); vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); vi.mock('@lib/local-dev', async (original) => ({ ...(await original()), @@ -59,7 +59,7 @@ it.each(['continue', 'exit'] as const)( }); // runWizard installs its own session first; auth then sets credentials, // and the agent dies after that. - vi.mocked(runAgent).mockImplementation(() => { + vi.mocked(runProgramAgent).mockImplementation(() => { store.setCredentials({ accessToken: 'tok', projectApiKey: 'pk', diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index a4fd1f037..10ebfa4ca 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -79,7 +79,7 @@ export function validateNonInteractiveOptions( * (`runWizardHeadless`) runs. * * Validates flags, builds a `ci:true` session, runs `config.ciPreRun` (or the - * program's `onReady` hooks by default), executes `runAgent`, and routes any + * program's `onReady` hooks by default), executes `runProgramAgent`, and routes any * failure through `wizardAbort`. `wizardAbort` owns all exits — never add a * raw `process.exit` here. * @@ -335,8 +335,10 @@ export function runNonInteractive( } } - const { runAgent } = await import('@lib/programs/run-agent-legacy'); - await runAgent(config, session); + const { runProgramAgent } = await import( + '@lib/programs/run-agent-legacy' + ); + await runProgramAgent(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 77384e6f2..42b257cd5 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,6 +1,6 @@ import { VERSION } from '@lib/version'; import { logToFile, getLogFilePath } from '@utils/debug'; -import { runAgent } from '@lib/programs/run-agent-legacy'; +import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { getProgramConfig } from '@lib/programs/program-registry'; import { getAuditChecks } from '@lib/programs/audit/types'; @@ -60,7 +60,7 @@ async function advanceStep( await step.run(await prepareRunSession(step, store.session)); store.completeRunStep(step.id); } else if (step.screenId === 'run') { - await runAgent(config, await prepareRunSession(step, store.session)); + await runProgramAgent(config, await prepareRunSession(step, store.session)); } else if (step.isComplete) { await store.waitUntil(step.isComplete); } @@ -262,7 +262,7 @@ export function runWizard( }); } else { try { - await runAgent(config, activeTui.store.session); + await runProgramAgent(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/status-history.ts b/src/lib/status-history.ts new file mode 100644 index 000000000..5931026c6 --- /dev/null +++ b/src/lib/status-history.ts @@ -0,0 +1,9 @@ +/** Shared FIFO window for agent snapshots and the terminal status panel. */ +export const MAX_STATUS_MESSAGES = 10; + +export function appendStatus(messages: string[], message: string): string[] { + if (messages.length > 0 && messages[messages.length - 1] === message) { + return messages; + } + return [...messages.slice(-(MAX_STATUS_MESSAGES - 1)), message]; +} diff --git a/src/ui/__tests__/agent-progress.test.ts b/src/ui/__tests__/agent-progress.test.ts new file mode 100644 index 000000000..b6bf8c49e --- /dev/null +++ b/src/ui/__tests__/agent-progress.test.ts @@ -0,0 +1,129 @@ +vi.mock('@ui', () => ({ getUI: vi.fn() })); +vi.mock('@utils/debug'); + +import { createUiReducer, uiInteraction } from '../agent-progress'; +import { LoggingUI } from '../logging-ui'; +import type { AgentProgress } from '@lib/agent/progress'; +import { OutroKind } from '@lib/wizard-session'; + +it('projects every progress event onto the matching UI call, in order', () => { + const ui = new LoggingUI(); + const calls: unknown[][] = []; + const methods = [ + 'startRun', + 'outro', + 'pushStatus', + 'syncTodos', + 'setStage', + 'setDashboardUrl', + 'setNotebookUrl', + 'addTokenUsage', + 'setFinalTokenCostUsd', + 'showAuthError', + 'setOutroData', + ] as const; + for (const method of methods) { + vi.spyOn(ui, method).mockImplementation(((...args: unknown[]) => { + calls.push([method, ...args]); + }) as never); + } + for (const level of ['info', 'warn', 'error', 'success', 'step'] as const) { + vi.spyOn(ui.log, level).mockImplementation((message) => { + calls.push([level, message]); + }); + } + const spinner = vi.spyOn(ui, 'spinner').mockReturnValue({ + start: (message) => { + calls.push(['spinner:start', message]); + }, + message: (message) => { + calls.push(['spinner:message', message]); + }, + stop: (message) => { + calls.push(['spinner:stop', message]); + }, + }); + const tasks = [{ content: 'Install', status: 'completed' }]; + const delta = { + inputTokens: 1, + outputTokens: 2, + cacheReadTokens: 3, + cacheCreationTokens: 4, + cacheCreation5m: 4, + cacheCreation1h: 0, + }; + const detail = { hasSettingsConflict: false, logFilePath: '/tmp/wizard.log' }; + const outro = { kind: OutroKind.Success, message: 'Finished' }; + const events: AgentProgress[] = [ + { kind: 'lifecycle', phase: 'started' }, + { kind: 'spinner', action: 'start', message: 'Starting' }, + { kind: 'spinner', action: 'message', message: 'Working' }, + { kind: 'spinner', action: 'stop' }, + ...(['info', 'warn', 'error', 'success', 'step'] as const).map((level) => ({ + kind: 'log' as const, + level, + message: level, + })), + { kind: 'status', message: 'Configured' }, + { kind: 'tasks', tasks }, + { kind: 'stage', stage: 'Install' }, + { kind: 'url', which: 'dashboard', url: 'https://d/1' }, + { kind: 'url', which: 'notebook', url: 'https://n/1' }, + { kind: 'usage', delta }, + { kind: 'finalCost', usd: 1.25 }, + { kind: 'authError', detail }, + { kind: 'completion', outro }, + { kind: 'lifecycle', phase: 'completed', message: 'Finished' }, + ]; + const reduce = createUiReducer(ui); + expect(spinner).not.toHaveBeenCalled(); + events.forEach(reduce); + expect(spinner).toHaveBeenCalledTimes(1); + expect(calls).toEqual([ + ['startRun'], + ['spinner:start', 'Starting'], + ['spinner:message', 'Working'], + ['spinner:stop', undefined], + ['info', 'info'], + ['warn', 'warn'], + ['error', 'error'], + ['success', 'success'], + ['step', 'step'], + ['pushStatus', 'Configured'], + ['syncTodos', tasks], + ['setStage', 'Install'], + ['setDashboardUrl', 'https://d/1'], + ['setNotebookUrl', 'https://n/1'], + ['addTokenUsage', delta], + ['setFinalTokenCostUsd', 1.25], + ['showAuthError', detail], + ['setOutroData', outro], + ['outro', 'Finished'], + ]); +}); + +it('forwards answers, notices and their dismissals', async () => { + const ui = new LoggingUI(); + const question = { id: 'q', source: 'test', questions: [] }; + const notice = { + title: 'Optional', + body: [], + items: [], + prompt: 'Continue?', + confirmLabel: 'Yes', + cancelLabel: 'No', + }; + const ask = vi.spyOn(ui, 'requestQuestion').mockResolvedValue({ q: 'yes' }); + const cancelAsk = vi.spyOn(ui, 'cancelPendingQuestion'); + const show = vi.spyOn(ui, 'showTaskNotice').mockResolvedValue(true); + const cancelNotice = vi.spyOn(ui, 'cancelTaskNotice'); + const interaction = uiInteraction(ui); + await expect(interaction.ask?.(question)).resolves.toEqual({ q: 'yes' }); + await expect(interaction.taskNotice?.(notice)).resolves.toBe(true); + interaction.cancelAsk?.(); + interaction.cancelTaskNotice?.(); + expect(ask).toHaveBeenCalledWith(question); + expect(show).toHaveBeenCalledWith(notice); + expect(cancelAsk).toHaveBeenCalledOnce(); + expect(cancelNotice).toHaveBeenCalledOnce(); +}); diff --git a/src/ui/agent-progress.ts b/src/ui/agent-progress.ts new file mode 100644 index 000000000..d3e3450cc --- /dev/null +++ b/src/ui/agent-progress.ts @@ -0,0 +1,71 @@ +import type { WizardUI, SpinnerHandle } from './wizard-ui'; +import type { AgentInteraction, AgentProgress } from '@lib/agent/progress'; + +// ── Progress → WizardUI, one call per event ─────────────────────────── + +/** + * The inverse of the agent's former `getUI()` calls: one event, one + * `WizardUI` method, synchronous, in emission order. Because each case maps + * back to exactly the call the agent used to make, the frame and flow goldens + * hold without regeneration. + */ +export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { + let spinner: SpinnerHandle | undefined; + return (event) => { + switch (event.kind) { + case 'lifecycle': + if (event.phase === 'started') ui.startRun(); + else ui.outro(event.message); + break; + case 'spinner': { + const handle = (spinner ??= ui.spinner()); + handle[event.action](event.message); + break; + } + case 'log': + ui.log[event.level](event.message); + break; + case 'status': + ui.pushStatus(event.message); + break; + case 'tasks': + ui.syncTodos(event.tasks); + break; + case 'stage': + ui.setStage(event.stage); + break; + case 'url': + if (event.which === 'dashboard') ui.setDashboardUrl(event.url); + else ui.setNotebookUrl(event.url); + break; + case 'usage': + ui.addTokenUsage(event.delta); + break; + case 'finalCost': + ui.setFinalTokenCostUsd(event.usd); + break; + case 'authError': + ui.showAuthError(event.detail); + break; + case 'completion': + ui.setOutroData(event.outro); + break; + default: { + const unhandled: never = event; + throw new Error( + `Unhandled agent progress: ${JSON.stringify(unhandled)}`, + ); + } + } + }; +} + +/** The agent's questions, answered wherever `getUI()` answers them today. */ +export function uiInteraction(ui: WizardUI): AgentInteraction { + return { + ask: (question) => ui.requestQuestion(question), + cancelAsk: () => ui.cancelPendingQuestion(), + taskNotice: (notice) => ui.showTaskNotice(notice), + cancelTaskNotice: () => ui.cancelTaskNotice(), + }; +} diff --git a/src/ui/tui/constants.ts b/src/ui/tui/constants.ts index 9c055375c..ae420b5c3 100644 --- a/src/ui/tui/constants.ts +++ b/src/ui/tui/constants.ts @@ -6,4 +6,4 @@ */ export const COLLAPSED_COUNT = 2; -export const EXPANDED_COUNT = 10; +export { MAX_STATUS_MESSAGES as EXPANDED_COUNT } from '@lib/status-history'; diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index 4e98e5d0b..f4214510e 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -57,7 +57,7 @@ import type { import { getProgramConfig } from '@lib/programs/program-registry'; 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 { appendStatus } from '@lib/status-history'; import { IS_DEV } from '@lib/constants'; import { computeTokenCostUsd } from '@lib/agent/token-pricing'; @@ -120,13 +120,6 @@ interface GateEntry { resolved: boolean; } -/** - * FIFO cap on retained status lines. The status bar is the only consumer and - * renders at most EXPANDED_COUNT lines, so there is no reason to retain more — - * the cap is tied to the window it feeds. - */ -const MAX_STATUS_MESSAGES = EXPANDED_COUNT; - // Capture blocked skill downloads once per readiness result. function captureHealthCheckBlocked(result: WizardReadinessResult): void { try { @@ -1052,15 +1045,8 @@ export class WizardStore { pushStatus(message: string): void { const msgs = this.$statusMessages.get(); - // Skip consecutive duplicate messages (no allocation on the hot path) - if (msgs.length > 0 && msgs[msgs.length - 1] === message) return; - // Nanostore detects change by reference equality, so a new array is - // required. At the cap, allocate exactly once at the final size (dropping - // the oldest entry) rather than push-then-truncate. - const next = - msgs.length >= MAX_STATUS_MESSAGES - ? [...msgs.slice(msgs.length - MAX_STATUS_MESSAGES + 1), message] - : [...msgs, message]; + const next = appendStatus(msgs, message); + if (next === msgs) return; this.$statusMessages.set(next); this.emitChange(); } From dbfbfff6e608a39299718bb467f4f05607e91ffd Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 19:06:59 -0400 Subject: [PATCH 05/61] fix(agent): pause writes during pending questions Track pending requests in the ask bridge and pass its accessor to both Anthropic harness paths. This restores the permission guard against Write/Edit calls racing an unanswered wizard_ask request. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__tests__/pending-question.test.ts | 201 ++++++++++++++++++ .../agent/runner/harness/anthropic/index.ts | 2 + src/lib/wizard-ask-bridge.ts | 9 + 3 files changed, 212 insertions(+) create mode 100644 src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts diff --git a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts b/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts new file mode 100644 index 000000000..4102211ea --- /dev/null +++ b/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts @@ -0,0 +1,201 @@ +import { initializeAgent, wizardCanUseTool } from '@lib/agent/agent-interface'; +import { createAskBridge } from '../../../shared/ask'; +import { anthropicBackend } from '..'; +import type { BackendRunInputs, TaskRunInputs } from '../../types'; +import type { AskAnswers } from '@lib/wizard-session'; +import { Harness, Sequence } from '@lib/constants'; +import { HostResolution } from '@lib/host-resolution'; + +vi.mock('@utils/analytics'); +vi.mock('@utils/debug'); +vi.mock('@lib/agent/aio-capture', () => ({ createAioCapture: vi.fn() })); +vi.mock('@lib/agent/agent-interface', async (original) => ({ + ...(await original()), + initializeAgent: vi.fn().mockResolvedValue({}), + runAgent: vi.fn().mockResolvedValue({}), +})); + +const questions = [{ id: 'q', prompt: 'Continue?', kind: 'text' as const }]; + +async function initializeHarness( + mode: 'linear' | 'task', + askBridge: BackendRunInputs['askBridge'], +) { + const credentials = { + accessToken: 'test', + projectApiKey: 'test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }; + const inputs: BackendRunInputs = { + config: { + programId: 'test', + run: { + integrationLabel: 'test', + spinnerMessage: 'Working', + successMessage: 'Done', + estimatedDurationMinutes: 1, + reportFile: 'report.md', + docsUrl: 'https://docs.test', + }, + composed: false, + binding: { + harness: Harness.anthropic, + sequence: Sequence.linear, + model: 'test', + }, + switchboard: { program: 'test', flags: {} }, + skillsBaseUrl: 'https://skills.test', + wizardFlags: {}, + wizardFlagPayloads: {}, + wizardMetadata: {}, + }, + input: { + installDir: '/test', + flags: { + ci: false, + signup: false, + debug: false, + e2eAsk: false, + localMcp: false, + captureAio: false, + benchmark: false, + yaraReport: false, + }, + host: {}, + credentials, + project: null, + apiUser: null, + }, + boot: { + programId: 'test', + skillsBaseUrl: 'https://skills.test', + credentials, + wizardFlags: {}, + wizardFlagPayloads: {}, + wizardMetadata: {}, + project: null, + triageProvider: undefined, + }, + emit: vi.fn(), + prompt: 'test', + spinner: { start: vi.fn(), stop: vi.fn(), message: vi.fn() }, + model: 'test', + askBridge, + }; + if (mode === 'linear') { + await anthropicBackend.run(inputs); + } else { + if (!anthropicBackend.runTask) throw new Error('Missing task harness'); + await anthropicBackend.runTask({ + ...inputs, + spinnerMessage: 'Working', + successMessage: 'Done', + additionalFeatureQueue: [], + requestRemark: false, + analyticsProperties: {}, + orchestrator: {} as TaskRunInputs['orchestrator'], + }); + } + const call = vi.mocked(initializeAgent).mock.calls.at(-1); + if (!call) throw new Error('Harness did not initialize the agent'); + const [config] = call; + return (behavior: 'allow' | 'deny') => { + for (const tool of ['Write', 'Edit']) { + expect( + wizardCanUseTool( + tool, + { file_path: 'src/app.ts', content: 'changed' }, + { wizardAskPending: config.getPendingQuestion?.() != null }, + ).behavior, + ).toBe(behavior); + } + }; +} + +afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); +}); + +describe.each(['linear', 'task'] as const)( + 'Anthropic %s pending-question permission guard', + (mode) => { + it.each(['answer', 'cancel', 'timeout', 'reject'] as const)( + 'blocks simultaneous Write/Edit and releases them after %s', + async (ending) => { + vi.useFakeTimers(); + let resolve!: (answers: AskAnswers) => void; + let reject!: (error: Error) => void; + const cancelAsk = vi.fn(); + const bridge = createAskBridge( + { + ask: () => { + expectPermission('deny'); + return new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + }, + cancelAsk, + }, + { getSource: () => 'test', richLinks: false, timeoutMs: 1000 }, + ); + if (!bridge) throw new Error('Missing ask bridge'); + const expectPermission = await initializeHarness(mode, bridge); + expectPermission('allow'); + const pending = bridge.request({ questions }); + expectPermission('deny'); + + if (ending === 'reject') { + const rejected = expect(pending).rejects.toThrow('answerer failed'); + reject(new Error('answerer failed')); + await rejected; + } else { + if (ending === 'timeout') { + await vi.advanceTimersByTimeAsync(1000); + } else { + resolve({ q: ending === 'answer' ? 'yes' : '__cancelled__' }); + } + await expect(pending).resolves.toEqual({ + answers: { q: ending === 'answer' ? 'yes' : '__cancelled__' }, + timedOut: ending === 'timeout', + }); + } + + expectPermission('allow'); + expect(cancelAsk).toHaveBeenCalledTimes(ending === 'timeout' ? 1 : 0); + expect(vi.getTimerCount()).toBe(0); + }, + ); + + it('keeps the guard when a concurrent question is rejected', async () => { + let resolve!: (answers: AskAnswers) => void; + const ask = vi + .fn() + .mockImplementationOnce( + () => new Promise((res) => (resolve = res)), + ) + .mockRejectedValueOnce(new Error('another request is pending')); + const bridge = createAskBridge( + { ask }, + { getSource: () => 'test', richLinks: false }, + ); + if (!bridge) throw new Error('Missing ask bridge'); + const expectPermission = await initializeHarness(mode, bridge); + const first = bridge.request({ questions }); + await expect(bridge.request({ questions })).rejects.toThrow( + 'another request is pending', + ); + expectPermission('deny'); + resolve({ q: 'yes' }); + await first; + expectPermission('allow'); + }); + + it('allows file mutations without an answerer', async () => { + const expectPermission = await initializeHarness(mode, undefined); + expectPermission('allow'); + }); + }, +); diff --git a/src/lib/agent/runner/harness/anthropic/index.ts b/src/lib/agent/runner/harness/anthropic/index.ts index 342175f8c..660c5808e 100644 --- a/src/lib/agent/runner/harness/anthropic/index.ts +++ b/src/lib/agent/runner/harness/anthropic/index.ts @@ -60,6 +60,7 @@ export const anthropicBackend: AgentHarness = { programId: boot.programId, integrationLabel: config.integrationLabel, askBridge, + getPendingQuestion: askBridge?.getPendingQuestion, askMaxQuestions: config.maxQuestions, allowedTools: runConfig.allowedTools, disallowedTools: runConfig.disallowedTools, @@ -141,6 +142,7 @@ export const anthropicBackend: AgentHarness = { // Only a task allowed to ask carries a bridge, so the Write/Edit pause // that rides on a pending question stays inside that task's agent. askBridge, + getPendingQuestion: askBridge?.getPendingQuestion, orchestrator, capture, emit, diff --git a/src/lib/wizard-ask-bridge.ts b/src/lib/wizard-ask-bridge.ts index 93b06cefa..9dc974d60 100644 --- a/src/lib/wizard-ask-bridge.ts +++ b/src/lib/wizard-ask-bridge.ts @@ -53,6 +53,8 @@ export interface AskResponse { export interface WizardAskBridge { /** Open the WizardAsk overlay and resolve with the user's answers. */ request(req: WizardAskRequest): Promise; + /** An unresolved question keeps file mutations paused across concurrent requests. */ + getPendingQuestion: () => PendingQuestion | null; } export interface WizardAskBridgeOptions { @@ -114,8 +116,13 @@ export function createWizardAskBridge( opts: WizardAskBridgeOptions, ): WizardAskBridge { const timeoutMs = opts.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS; + const pendingQuestions = new Map(); return { + getPendingQuestion: () => { + for (const pending of pendingQuestions.values()) return pending; + return null; + }, async request({ questions, subject }) { const pending: PendingQuestion = { id: randomUUID(), @@ -124,6 +131,7 @@ export function createWizardAskBridge( richLinks: opts.richLinks ?? false, askedAt: new Date().toISOString(), }; + pendingQuestions.set(pending.id, pending); const startedAt = Date.now(); let timer: ReturnType | undefined; @@ -168,6 +176,7 @@ export function createWizardAskBridge( return { answers, timedOut }; } finally { if (timer) clearTimeout(timer); + pendingQuestions.delete(pending.id); } }, }; From d2142164aafc4dece5a4cd89fcc88e12ffd94efd Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 19:40:49 -0400 Subject: [PATCH 06/61] test(agent): preserve question wait behavior Cover delayed answers across both sequences and harness bindings, plus completion when questions are disabled. Production behavior is unchanged. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__tests__/run-agent-standalone.test.ts | 121 ++++++++++++++++-- 1 file changed, 108 insertions(+), 13 deletions(-) diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts index 13f8bc458..481e8a5eb 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -11,7 +11,11 @@ import * as os from 'os'; import * as path from 'path'; import { Harness, Sequence, DEFAULT_AGENT_MODEL } from '@lib/constants'; import { HostResolution } from '@lib/host-resolution'; -import { OutroKind, type PendingQuestion } from '@lib/wizard-session'; +import { + OutroKind, + type AskAnswers, + type PendingQuestion, +} from '@lib/wizard-session'; import { AGENT_ERROR_CODE, ErrorCodes } from '@lib/errors'; import { AgentErrorType } from '@lib/agent/signals'; import type { AgentFailure } from '@lib/agent/runner/shared/types'; @@ -31,6 +35,7 @@ vi.mock('@ui', () => ({ }, })); vi.mock('@utils/debug'); +vi.mock('@utils/terminal-bell'); vi.mock('@lib/yara-hooks', async (original) => ({ ...(await original()), flushScanReport: vi.fn(), @@ -69,9 +74,19 @@ const harnessState = vi.hoisted(() => ({ askQuestions: undefined as PendingQuestion['questions'] | undefined, })); vi.mock('@lib/agent/runner/switchboard/harness', () => { + const askIfRequested = async (inputs: BackendRunInputs | TaskRunInputs) => { + if (!harnessState.askQuestions || !inputs.askBridge) return; + const { answers } = await inputs.askBridge.request({ + questions: harnessState.askQuestions, + }); + inputs.emit({ + kind: 'status', + message: `answered:${JSON.stringify(answers)}`, + }); + }; const fake: AgentHarness = { name: Harness.pi, - runTask(inputs: TaskRunInputs) { + async runTask(inputs: TaskRunInputs) { harnessState.tasks.push(inputs); const { store, currentTaskId } = inputs.orchestrator; if (!currentTaskId) { @@ -81,6 +96,7 @@ vi.mock('@lib/agent/runner/switchboard/harness', () => { } else if (harnessState.taskFailure) { return Promise.resolve({ failure: harnessState.taskFailure }); } else { + await askIfRequested(inputs); store.complete(currentTaskId, { goals: 'install', did: 'installed', @@ -114,15 +130,7 @@ vi.mock('@lib/agent/runner/switchboard/harness', () => { cacheCreation1h: 0, }, }); - if (harnessState.askQuestions && inputs.askBridge) { - const { answers } = await inputs.askBridge.request({ - questions: harnessState.askQuestions, - }); - emit({ - kind: 'status', - message: `answered:${JSON.stringify(answers)}`, - }); - } + await askIfRequested(inputs); if (harnessState.throws) throw harnessState.throws; spinner.stop('Done'); return harnessState.result as never; @@ -157,7 +165,7 @@ vi.mock('@lib/agent/agent-prompt-loader', async (original) => { 'test-program', ), actual.parseAgentPrompt( - '---\ntype: install\n---\nInstall it', + '---\ntype: install\nallowedTools: [wizard_ask]\n---\nInstall it', 'install', 'test-program', ), @@ -173,7 +181,7 @@ vi.mock('@lib/wizard-tools', async (original) => ({ fetchSkillMenu: vi.fn().mockResolvedValue({ categories: {} }), })); -import { runAgent } from '@lib/agent/runner'; +import { runAgent, RunOutcome } from '@lib/agent/runner'; import type { RunConfig, RunInput } from '@lib/agent/runner'; import { analytics } from '@utils/analytics'; import { initLogFile } from '@utils/debug'; @@ -253,6 +261,93 @@ beforeEach(() => { afterEach(() => fs.rmSync(tmp, { recursive: true, force: true })); describe('runAgent standalone', () => { + it.each([ + [Harness.pi, Sequence.linear], + [Harness.anthropic, Sequence.linear], + [Harness.pi, Sequence.orchestrator], + [Harness.anthropic, Sequence.orchestrator], + ])( + 'waits for the answer before completing %s %s', + async (harness, sequence) => { + harnessState.askQuestions = [ + { id: 'q1', prompt: 'Continue?', kind: 'text' }, + ]; + let release: ((answers: AskAnswers) => void) | undefined; + const ask = vi.fn( + () => + new Promise((resolve) => { + release = resolve; + }), + ); + const events: AgentProgress[] = []; + let settled = false; + const running = runAgent( + config({ + binding: { harness, sequence, model: DEFAULT_AGENT_MODEL }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: harness, + }, + }), + input(), + { interaction: { ask }, onProgress: (event) => events.push(event) }, + ).then((result) => { + settled = true; + return result; + }); + + try { + await vi.waitFor(() => expect(ask).toHaveBeenCalledTimes(1)); + await new Promise((resolve) => setImmediate(resolve)); + expect(settled).toBe(false); + expect(events.some((event) => event.kind === 'completion')).toBe(false); + expect(analytics.shutdown).not.toHaveBeenCalled(); + expect(flushScanReport).not.toHaveBeenCalled(); + } finally { + release?.({ q1: 'yes' }); + } + + const result = await running; + expect(result.outcome).toBe(RunOutcome.Success); + expect(result.snapshot.statusMessages).toContain('answered:{"q1":"yes"}'); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); + expect(flushScanReport).toHaveBeenCalledTimes(1); + }, + ); + + it.each([Sequence.linear, Sequence.orchestrator])( + 'does not wait for disabled questions in %s', + async (sequence) => { + harnessState.askQuestions = [ + { id: 'q1', prompt: 'Continue?', kind: 'text' }, + ]; + const ask = vi.fn(() => + Promise.reject(new Error('disabled answerer called')), + ); + const runConfig = config({ + binding: { harness: Harness.pi, sequence, model: DEFAULT_AGENT_MODEL }, + }); + const runInput = input(); + runInput.flags.ci = true; + vi.stubEnv('WIZARD_ASK_AUTODRIVE', ''); + try { + const result = await runAgent(runConfig, runInput, { + interaction: { ask }, + }); + expect(result.outcome).toBe(RunOutcome.Success); + expect(ask).not.toHaveBeenCalled(); + const inputs = + sequence === Sequence.linear + ? (harnessState.lastInputs as BackendRunInputs) + : harnessState.tasks.at(-1); + expect(inputs?.askBridge).toBeUndefined(); + } finally { + vi.unstubAllEnvs(); + } + }, + ); + it.each([Harness.pi, Harness.anthropic])( 'dispatches %s through both sequence arms', async (harness) => { From a778bcdb459065a4e7aa104366eaef23b4917be8 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 20:00:59 -0400 Subject: [PATCH 07/61] docs(agent): add runner boundary diagram Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- docs/images/wizard-agent-boundary.png | Bin 0 -> 136119 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/wizard-agent-boundary.png diff --git a/docs/images/wizard-agent-boundary.png b/docs/images/wizard-agent-boundary.png new file mode 100644 index 0000000000000000000000000000000000000000..1378135853198e832bb1e0cb7282206e9ade598a GIT binary patch literal 136119 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 NDLqqHD3-Sf`Y$ICo67(I literal 0 HcmV?d00001 From c001f46774e02ab87a19773c3634a2c8a6b8dd4e Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 20:18:01 -0400 Subject: [PATCH 08/61] docs(agent): remove diagram image Keep the diagram as native Mermaid in the PR description. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- docs/images/wizard-agent-boundary.png | Bin 136119 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/wizard-agent-boundary.png diff --git a/docs/images/wizard-agent-boundary.png b/docs/images/wizard-agent-boundary.png deleted file mode 100644 index 1378135853198e832bb1e0cb7282206e9ade598a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136119 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 NDLqqHD3-Sf`Y$ICo67(I From 3be61a8615cf3b2808171d65aa515fc6fa8b4dcd Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:13:33 -0400 Subject: [PATCH 09/61] refactor(errors): move WizardError next to the error codes The agent builds WizardError for every decided failure, and it had to import the process-exit module to do so. The class now lives in src/lib/errors/wizard-error.ts; wizard-abort re-exports it so every other importer is unchanged. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/agent/agent-interface.ts | 3 +-- src/lib/agent/runner/sequence/linear.ts | 3 +-- .../orchestrator/orchestrator-runner.ts | 3 +-- src/lib/agent/runner/shared/errors.ts | 2 +- src/lib/errors/index.ts | 1 + src/lib/errors/wizard-error.ts | 21 +++++++++++++++++++ src/lib/gateway-session.ts | 3 +-- src/utils/wizard-abort.ts | 21 +++++-------------- 8 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 src/lib/errors/wizard-error.ts diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 602923329..45bd05705 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -30,7 +30,6 @@ import { type AdditionalFeature, ADDITIONAL_FEATURE_PROMPTS, } from '@lib/wizard-session'; -import { WizardError } from '@utils/wizard-abort'; import type { AgentFailure } from './runner/shared/types'; import { createCustomHeaders } from '@utils/custom-headers'; import type { HostResolution } from '@lib/host-resolution'; @@ -58,7 +57,7 @@ import { REMARK_INSTRUCTION, RESUME_INSTRUCTION, } from './signals'; -import { classifyAuthFailure } from '@lib/errors'; +import { classifyAuthFailure, WizardError } from '@lib/errors'; import { isGrantRevoked } from '@lib/auth-session-state'; import { AgentOutputSignals } from './output-signals'; diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index cbdc169dc..479330533 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -14,8 +14,7 @@ import { OutroKind, type OutroData } from '@lib/wizard-session'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; import { logToFile } from '../../../../utils/debug'; import { createBenchmarkPipeline } from '../../../middleware/benchmark'; -import { WizardError } from '../../../../utils/wizard-abort'; -import { ErrorCodes, AGENT_ERROR_CODE } from '@lib/errors'; +import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@lib/errors'; import { analytics } from '../../../../utils/analytics'; import { formatYaraAbortMessage } from '../../../yara-hooks'; import { installSkillById } from '../../../wizard-tools'; diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 43c41c137..86ac138cd 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -33,8 +33,7 @@ import { analytics } from '@utils/analytics'; import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; -import { WizardError } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes, WizardError } from '@lib/errors'; import type { AgentInteraction } from '@lib/agent/progress'; import type { AgentFailure, diff --git a/src/lib/agent/runner/shared/errors.ts b/src/lib/agent/runner/shared/errors.ts index 8578d2a0c..d991e4242 100644 --- a/src/lib/agent/runner/shared/errors.ts +++ b/src/lib/agent/runner/shared/errors.ts @@ -4,7 +4,7 @@ import type { InstallSkillResult } from '@lib/wizard-tools'; import { skillErrorCode } from '@lib/errors'; -import { WizardError } from '@utils/wizard-abort'; +import { WizardError } from '@lib/errors'; import { RunOutcome, type AgentFailure, type SequenceResult } from './types'; export const failed = (failure: AgentFailure): SequenceResult => ({ diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index 5558c7942..51a0793d7 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -5,6 +5,7 @@ export { type ErrorCode, } from './codes'; export { ERROR_CATALOG } from './catalog'; +export { WizardError } from './wizard-error'; export type { ErrorCatalogEntry, ErrorGroup, RetryAdvice } from './types'; export { classifyAuthFailure, type AuthFailureInput } from './auth'; export { AGENT_ERROR_CODE } from './agent-map'; diff --git a/src/lib/errors/wizard-error.ts b/src/lib/errors/wizard-error.ts new file mode 100644 index 000000000..65537f2ef --- /dev/null +++ b/src/lib/errors/wizard-error.ts @@ -0,0 +1,21 @@ +import type { ErrorCode } from './codes'; + +/** + * Structured error data for analytics and the machine-readable error line. + * + * A data carrier: the agent returns it inside a failure and the legacy adapter + * hands it to `wizardAbort()`, which captures it. Never thrown by the wizard. + */ +export class WizardError extends Error { + readonly code?: ErrorCode; + + constructor( + message: string, + public readonly context?: Record, + code?: ErrorCode, + ) { + super(message); + this.name = 'WizardError'; + this.code = code; + } +} diff --git a/src/lib/gateway-session.ts b/src/lib/gateway-session.ts index 577753df2..55d50d651 100644 --- a/src/lib/gateway-session.ts +++ b/src/lib/gateway-session.ts @@ -9,8 +9,7 @@ import { readFileSync } from 'node:fs'; import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; -import { WizardError } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes, WizardError } from '@lib/errors'; import type { HostResolution } from '@lib/host-resolution'; import { checkLlmGatewayHealth } from '@lib/health-checks/endpoints'; import { ServiceHealthStatus } from '@lib/health-checks/types'; diff --git a/src/utils/wizard-abort.ts b/src/utils/wizard-abort.ts index 7805edcc4..f5973a745 100644 --- a/src/utils/wizard-abort.ts +++ b/src/utils/wizard-abort.ts @@ -3,7 +3,7 @@ * * Sequence: cleanup -> error capture (optional) -> analytics shutdown -> outro -> process.exit * - * WizardError is a data carrier passed to wizardAbort() for analytics context, never thrown. + * WizardError (from `@lib/errors`) is a data carrier passed to wizardAbort() for analytics context, never thrown. * The legacy abort() in setup-utils.ts delegates here. */ import { analytics } from './analytics'; @@ -12,21 +12,10 @@ 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'; - -export class WizardError extends Error { - readonly code?: ErrorCode; - - constructor( - message: string, - public readonly context?: Record, - code?: ErrorCode, - ) { - super(message); - this.name = 'WizardError'; - this.code = code; - } -} +import { WizardError, emitWizardError, sanitizeErrorDetail } from '@lib/errors'; + +// Still importable from here; the class lives with the error codes. +export { WizardError }; interface WizardAbortOptions { message?: string; From 9ce789977408e7636c1b63243bcc3d87bdc5785d Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:14:59 -0400 Subject: [PATCH 10/61] refactor(agent): own the progress payload types TokenUsageDelta, SpinnerHandle and AuthErrorDetail are what the agent puts on its progress events, and AgentChunk is what the streaming prompt runner yields. They move next to the code that produces them; wizard-ui.ts and the MCP prompts service re-export them so every UI importer keeps its path. The agent no longer imports from src/ui for these. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/agent/mcp-prompt-streaming.ts | 18 +++++- src/lib/agent/progress.ts | 62 +++++++++++++++++-- src/lib/middleware/benchmark.ts | 3 +- src/lib/middleware/benchmarks/summary.ts | 3 +- src/lib/middleware/types.ts | 2 +- .../mcp-suggested-prompts-services.ts | 20 ++---- src/ui/wizard-ui.ts | 61 +++--------------- 7 files changed, 89 insertions(+), 80 deletions(-) diff --git a/src/lib/agent/mcp-prompt-streaming.ts b/src/lib/agent/mcp-prompt-streaming.ts index 1b9e55182..62bc48e48 100644 --- a/src/lib/agent/mcp-prompt-streaming.ts +++ b/src/lib/agent/mcp-prompt-streaming.ts @@ -12,7 +12,6 @@ * `for await (...)` and render as they arrive. */ -import type { AgentChunk } from '@ui/tui/services/mcp-suggested-prompts-services'; import type { Credentials } from '@lib/wizard-session'; import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@lib/constants'; import { logToFile } from '@utils/debug'; @@ -22,6 +21,23 @@ import { sanitizeAgentSubprocessEnv } from '@lib/agent/agent-env-isolation'; import { createIsolatedAgentConfigDir } from '@lib/agent/stored-login'; import { analytics } from '@utils/analytics'; +/** + * Discriminated union covering every kind of streamed event the screen + * needs to render. Production yields these from Claude SDK messages; + * the playground yields them from canned scripts. + */ +export type AgentChunk = + | { kind: 'text'; text: string } + /** `command` carries CLI mode's exec command string (`call …`) so the + * screen can recover the inner tool for context-aware follow-ups. */ + | { kind: 'tool-call'; toolName: string; detail: string; command?: string } + | { kind: 'tool-result'; toolName: string; detail: string } + | { kind: 'error'; text: string } + /** Stream completed. `sessionId` is the SDK session ID of the just- + * completed turn; pass it back as `resumeSessionId` on a follow-up + * call to continue the conversation with full history. */ + | { kind: 'done'; sessionId?: string }; + // Cached SDK module — first call pays the dynamic-import cost; later // calls reuse the same module. // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index 8be0d52f2..21550a1d6 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -15,13 +15,63 @@ import type { PendingQuestion, TaskNotice, } from '@lib/wizard-session'; -import type { - AuthErrorDetail, - SpinnerHandle, - TokenUsageDelta, -} from '@ui/wizard-ui'; +import type { SettingsConflict } from './claude-settings'; + +/** + * One assistant turn's token usage, for the hidden Ctrl+T token/cost HUD. + * `model` is the model that produced *this* turn (e.g. the SDK's + * `message.message.model`) — a subagent can run on a different model than + * the main session, and some programs override to Haiku, so pricing must key + * off the per-turn model rather than a single run-wide assumption. Omit only + * when the caller genuinely has no model context (falls back to Sonnet + * pricing — see `pricePerMtokForModel` in `@lib/agent/token-pricing`). + */ +export interface TokenUsageDelta { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheCreationTokens: number; + cacheCreation5m: number; + cacheCreation1h: number; + model?: string; +} + +/** The run spinner as the agent drives it: `WizardUI.spinner()` returns one. */ +export interface SpinnerHandle { + start(message?: string): void; + stop(message?: string): void; + message(msg?: string): void; +} -export type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta }; +/** + * Context the agent attaches to a 401 so the host can pick the right copy. + * + * `hasSettingsConflict` is true when a Claude Code settings file (project, + * project-local, the user's global config, or managed) actually overrides the + * LLM Gateway auth. `conflicts` carries the exact files and keys so the screen + * can name them. When there is no conflict, the 401 has a different cause (bad + * PAT prefix, missing scope, expired key, region mismatch) and we should not + * advise the user to log out of Claude Code. + */ +export interface AuthErrorDetail { + hasSettingsConflict: boolean; + conflicts?: SettingsConflict[]; + /** + * True when the agent SDK authenticated from a stored Claude login + * (`apiKeySource: "/login managed key"`) instead of the wizard's gateway + * token — conflicting Anthropic credentials. Takes priority in the screen. + */ + usingManagedLogin?: boolean; + /** Human-readable places a conflicting Anthropic credential may live. */ + credentialPlaces?: string[]; + /** + * True when a pre-run refresh already failed on a dead grant. The login is + * gone and re-running is the only fix, so this outranks every other branch — + * none of the usual advice (key type, scopes, region) applies. + */ + sessionExpired?: boolean; + logFilePath: string; +} /** One task as the host renders it. The same shape `WizardUI.syncTodos` takes. */ export interface TaskSnapshot { diff --git a/src/lib/middleware/benchmark.ts b/src/lib/middleware/benchmark.ts index 20017ad75..bc9fc3e7b 100644 --- a/src/lib/middleware/benchmark.ts +++ b/src/lib/middleware/benchmark.ts @@ -7,7 +7,8 @@ * pipeline.finalize(resultMessage, durationMs); */ -import { getUI, type SpinnerHandle } from '@ui'; +import { getUI } from '@ui'; +import type { SpinnerHandle } from '@lib/agent/progress'; import { logToFile, getLogFilePath, configureLogFile } from '@utils/debug'; import { MiddlewarePipeline } from './pipeline'; import { PhaseDetector } from './phase-detector'; diff --git a/src/lib/middleware/benchmarks/summary.ts b/src/lib/middleware/benchmarks/summary.ts index 45c3b0a8e..1900f9cc5 100644 --- a/src/lib/middleware/benchmarks/summary.ts +++ b/src/lib/middleware/benchmarks/summary.ts @@ -1,4 +1,5 @@ -import { getUI, type SpinnerHandle } from '@ui'; +import { getUI } from '@ui'; +import type { SpinnerHandle } from '@lib/agent/progress'; import { AgentSignals } from '@lib/agent/agent-interface'; import type { Middleware, diff --git a/src/lib/middleware/types.ts b/src/lib/middleware/types.ts index ea1715700..9e84580e0 100644 --- a/src/lib/middleware/types.ts +++ b/src/lib/middleware/types.ts @@ -5,7 +5,7 @@ * and can publish data to a shared store for downstream middleware to read. */ -import type { SpinnerHandle } from '@ui'; +import type { SpinnerHandle } from '@lib/agent/progress'; export type SDKMessage = any; diff --git a/src/ui/tui/services/mcp-suggested-prompts-services.ts b/src/ui/tui/services/mcp-suggested-prompts-services.ts index 8b41505ca..484a882a8 100644 --- a/src/ui/tui/services/mcp-suggested-prompts-services.ts +++ b/src/ui/tui/services/mcp-suggested-prompts-services.ts @@ -21,22 +21,10 @@ import { } from '@lib/mcp-project-profile'; import { seedDemoEvents as runSeed } from '@lib/mcp-seed-events'; -/** - * Discriminated union covering every kind of streamed event the screen - * needs to render. Production yields these from Claude SDK messages; - * the playground yields them from canned scripts. - */ -export type AgentChunk = - | { kind: 'text'; text: string } - /** `command` carries CLI mode's exec command string (`call …`) so the - * screen can recover the inner tool for context-aware follow-ups. */ - | { kind: 'tool-call'; toolName: string; detail: string; command?: string } - | { kind: 'tool-result'; toolName: string; detail: string } - | { kind: 'error'; text: string } - /** Stream completed. `sessionId` is the SDK session ID of the just- - * completed turn; pass it back as `resumeSessionId` on a follow-up - * call to continue the conversation with full history. */ - | { kind: 'done'; sessionId?: string }; +// The streamed event shape is the agent's; re-exported so the screen and the +// playground keep their import path. +import type { AgentChunk } from '@lib/agent/mcp-prompt-streaming'; +export type { AgentChunk }; export interface McpSuggestedPromptsServices { /** diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index 38ad29235..17f2792f3 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -29,60 +29,13 @@ export function isTaskStatus(value: string): value is TaskStatus { return (Object.values(TaskStatus) as string[]).includes(value); } -/** - * One assistant turn's token usage, for the hidden Ctrl+T token/cost HUD. - * `model` is the model that produced *this* turn (e.g. the SDK's - * `message.message.model`) — a subagent can run on a different model than - * the main session, and some programs override to Haiku, so pricing must key - * off the per-turn model rather than a single run-wide assumption. Omit only - * when the caller genuinely has no model context (falls back to Sonnet - * pricing — see `pricePerMtokForModel` in `@lib/agent/token-pricing`). - */ -export interface TokenUsageDelta { - inputTokens: number; - outputTokens: number; - cacheReadTokens: number; - cacheCreationTokens: number; - cacheCreation5m: number; - cacheCreation1h: number; - model?: string; -} - -export interface SpinnerHandle { - start(message?: string): void; - stop(message?: string): void; - message(msg?: string): void; -} - -/** - * Context passed to `showAuthError` so the screen can pick the right copy. - * - * `hasSettingsConflict` is true when a Claude Code settings file (project, - * project-local, the user's global config, or managed) actually overrides the - * LLM Gateway auth. `conflicts` carries the exact files and keys so the screen - * can name them. When there is no conflict, the 401 has a different cause (bad - * PAT prefix, missing scope, expired key, region mismatch) and we should not - * advise the user to log out of Claude Code. - */ -export interface AuthErrorDetail { - hasSettingsConflict: boolean; - conflicts?: SettingsConflict[]; - /** - * True when the agent SDK authenticated from a stored Claude login - * (`apiKeySource: "/login managed key"`) instead of the wizard's gateway - * token — conflicting Anthropic credentials. Takes priority in the screen. - */ - usingManagedLogin?: boolean; - /** Human-readable places a conflicting Anthropic credential may live. */ - credentialPlaces?: string[]; - /** - * True when a pre-run refresh already failed on a dead grant. The login is - * gone and re-running is the only fix, so this outranks every other branch — - * none of the usual advice (key type, scopes, region) applies. - */ - sessionExpired?: boolean; - logFilePath: string; -} +// Progress payloads are the agent's contract; re-exported so UI code keeps its import path. +import type { + AuthErrorDetail, + SpinnerHandle, + TokenUsageDelta, +} from '@lib/agent/progress'; +export type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta }; export interface WizardUI { // ── Lifecycle messages ──────────────────────────────────────────── From f15dc7a46af6957af6c2656d96bcb8085a439985 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:18:27 -0400 Subject: [PATCH 11/61] refactor(agent): own the outro, question and task-notice shapes OutroKind, OutroData, AskQuestion, AskAnswers, PendingQuestion and TaskNotice are what runAgent returns and asks with, so they move into src/lib/agent/progress.ts. Credentials moves next to the API types, AdditionalFeature next to the other program enums in constants, and the session's CloudRegion copy points at the one in @utils/types. wizard-session.ts re-exports all of them, so its 127 importers are unchanged. The agent no longer imports @lib/wizard-session for a shape; the one remaining WizardSession reference is ProgramRun's session hooks. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../architecture/known-violations.json | 4 +- src/lib/agent/agent-interface.ts | 12 +- src/lib/agent/mcp-prompt-streaming.ts | 2 +- src/lib/agent/progress.ts | 124 +++++++++++- src/lib/agent/runner/harness/types.ts | 2 +- src/lib/agent/runner/sequence/linear.ts | 2 +- .../orchestrator/orchestrator-runner.ts | 2 +- src/lib/agent/runner/shared/types.ts | 13 +- src/lib/api.ts | 25 +++ src/lib/constants.ts | 15 ++ src/lib/wizard-ask-bridge.ts | 2 +- src/lib/wizard-session.ts | 188 +++--------------- 12 files changed, 205 insertions(+), 186 deletions(-) diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index d83527e0a..57c17d063 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -2,7 +2,6 @@ "violations": [ "src/commands/factories/family-picker.tsx -> src/commands/command.ts", "src/env.ts -> src/lib/headless-mode.ts", - "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", @@ -55,7 +54,9 @@ "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-ask-bridge.ts -> src/lib/agent/progress.ts", "src/lib/wizard-session.ts -> src/lib/agent/claude-settings.ts", + "src/lib/wizard-session.ts -> src/lib/agent/progress.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", @@ -75,6 +76,7 @@ "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/ui/wizard-ui.ts -> src/lib/agent/progress.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" diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 45bd05705..3e7a17ad4 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -25,11 +25,9 @@ import { wizardUserAgentForProgram, DEFAULT_AGENT_MODEL, AWS_SKILLS_BASE_URL, -} from '@lib/constants'; -import { type AdditionalFeature, ADDITIONAL_FEATURE_PROMPTS, -} from '@lib/wizard-session'; +} from '@lib/constants'; import type { AgentFailure } from './runner/shared/types'; import { createCustomHeaders } from '@utils/custom-headers'; import type { HostResolution } from '@lib/host-resolution'; @@ -231,9 +229,7 @@ export type AgentConfig = { * Read accessor for the active pending question. Used by canUseTool to * block Write/Edit while the overlay is open (defense in depth). */ - getPendingQuestion?: () => - | import('@lib/wizard-session').PendingQuestion - | null; + getPendingQuestion?: () => import('./progress').PendingQuestion | null; /** * Orchestrator queue context. Present only when the `wizard-orchestrator` * flag routes the run here; threaded into wizard-tools so the orchestrator @@ -337,9 +333,7 @@ type AgentRunConfig = { * Read accessor for the active pending question. canUseTool reads this * to block Write/Edit while the overlay is open. */ - getPendingQuestion?: () => - | import('@lib/wizard-session').PendingQuestion - | null; + getPendingQuestion?: () => import('./progress').PendingQuestion | null; /** * The orchestrator owns the TUI task panel (it renders its queue), so its * runs suppress the agent's own TaskCreate/TaskUpdate rendering. Set from diff --git a/src/lib/agent/mcp-prompt-streaming.ts b/src/lib/agent/mcp-prompt-streaming.ts index 62bc48e48..f3d0354ec 100644 --- a/src/lib/agent/mcp-prompt-streaming.ts +++ b/src/lib/agent/mcp-prompt-streaming.ts @@ -12,7 +12,7 @@ * `for await (...)` and render as they arrive. */ -import type { Credentials } from '@lib/wizard-session'; +import type { Credentials } from '@lib/api'; import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@lib/constants'; import { logToFile } from '@utils/debug'; import { gatewayAuth } from '@lib/gateway-session'; diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index 21550a1d6..f99962a4a 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -9,14 +9,126 @@ * every existing runner is unchanged. */ -import type { - AskAnswers, - OutroData, - PendingQuestion, - TaskNotice, -} from '@lib/wizard-session'; import type { SettingsConflict } from './claude-settings'; +// ── What the agent hands back and asks with ───────────────────────── + +/** Outcome kind for the outro screen */ +export enum OutroKind { + Success = 'success', + Error = 'error', + Cancel = 'cancel', +} + +export interface OutroData { + kind: OutroKind; + /** Main headline (green check for Success, red X for Error, etc.) */ + message?: string; + /** Free-form body text shown under the headline. Use \n for paragraph breaks. */ + body?: string; + /** Success-only: bulleted list of "what the agent did" */ + changes?: string[]; + /** + * Success-only: a prominent, labeled link to where the user should go + * next (e.g. an inbox the program just configured). Rendered right under + * the headline and shown verbatim — no UTM tagging — so the URL stays + * clean and copy-pasteable. Set per-program in buildOutroData. + */ + primaryLink?: { label: string; url: string }; + /** + * Success-only: a short "what to do next" checklist with its own heading, + * rendered as a bulleted list. Distinct from `changes`, which recaps what + * the agent already did. + */ + nextSteps?: { heading: string; items: string[] }; + docsUrl?: string; + continueUrl?: string; + /** Report file the agent wrote (e.g. "posthog-setup-report.md") */ + reportFile?: string; + /** Stable machine-readable error code from the error catalog (@lib/errors). */ + errorCode?: import('@lib/errors').ErrorCode; + /** Structured context for the error code; safe for telemetry payloads. */ + errorDetail?: Record; + /** PostHog dashboard URL the program created on the user's behalf. */ + dashboardUrl?: string; + /** PostHog notebook URL the program uploaded the report to. */ + notebookUrl?: string; + /** + * Copy-paste prompt the operator hands to their coding agent to finish the + * job (work the report's checklist). Printed to the terminal's main buffer on + * exit (see getExitLine in start-tui.ts) — the TUI's alternate screen is wiped + * on exit, so the scrollback line is where it survives and can be + * triple-click-selected. Set per-program in buildOutroData. + */ + handoffPrompt?: string; +} + +/** A single question rendered by the WizardAsk overlay. */ +export interface AskQuestion { + /** Key for the response map */ + id: string; + prompt: string; + /** text = single-line free input; single/multi = picker */ + kind: 'single' | 'multi' | 'text'; + /** Required for `single` and `multi`. Ignored for `text`. */ + options?: { label: string; value: string; description?: string }[]; + /** Defaults to true */ + required?: boolean; + /** + * Only meaningful for kind='text'. When true, the wizard-tools `wizard_ask` + * tool stores the user's answer in the session secret vault and returns + * `{ secretRef }` to the agent instead of the plain string — so the value + * never enters the LLM conversation. The TUI masks the input as it is typed + * (see `shouldMaskAnswer`). See `secret-vault.ts`. + */ + sensitive?: boolean; +} + +/** + * Copy for a modal shown before an optional step runs, so the user can decline + * it. The program that owns the step supplies the words; the runner and the + * screen only carry them. + */ +export interface TaskNotice { + title: string; + /** Paragraphs, in order. */ + body: string[]; + /** Optional highlighted list, e.g. what was detected. */ + items?: string[]; + docsLabel?: string; + docsUrl?: string; + confirmLabel: string; + cancelLabel: string; + prompt: string; +} + +/** Map of question id → answer (string for single/text, string[] for multi). */ +export type AskAnswers = Record; + +/** A pending wizard_ask request held by the store. */ +export interface PendingQuestion { + id: string; + questions: AskQuestion[]; + /** + * UTC ISO 8601 timestamp of when the ask was created. Published on the + * task stream as `pending_input.asked_at` so the web app can age the + * prompt; stable across pushes for the lifetime of one ask. + */ + askedAt?: string; + /** Skill id of the caller. Set by the wizard from session.skillId. */ + source: string; + /** + * When true, the ask overlay renders standalone URLs in prompt text as + * OSC 8 hyperlinks and copies a lone URL to the clipboard. Opt-in per + * program (set from `ProgramRun.richLinks` via the ask bridge); defaults + * to false so existing flows render prompts exactly as before. See + * `LinkText` / `link-helpers`. + */ + richLinks?: boolean; +} + +// ── What the agent reports ────────────────────────────────────────── + /** * One assistant turn's token usage, for the hidden Ctrl+T token/cost HUD. * `model` is the model that produced *this* turn (e.g. the SDK's diff --git a/src/lib/agent/runner/harness/types.ts b/src/lib/agent/runner/harness/types.ts index 35c082c21..cff145347 100644 --- a/src/lib/agent/runner/harness/types.ts +++ b/src/lib/agent/runner/harness/types.ts @@ -19,7 +19,7 @@ * the run's result. */ -import type { AdditionalFeature } from '@lib/wizard-session'; +import type { AdditionalFeature } from '@lib/constants'; import type { Harness } from '@lib/constants'; import type { WizardAskBridge } from '@lib/wizard-ask-bridge'; import type { AgentErrorType } from '@lib/agent/agent-interface'; diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index 479330533..3f75d0c5e 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -10,7 +10,7 @@ * arguments, so the caller's exit sequence is unchanged. */ -import { OutroKind, type OutroData } from '@lib/wizard-session'; +import { OutroKind, type OutroData } from '@lib/agent/progress'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; import { logToFile } from '../../../../utils/debug'; import { createBenchmarkPipeline } from '../../../middleware/benchmark'; diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 86ac138cd..43a3666a7 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -22,7 +22,7 @@ import { writeFileSync, } from 'fs'; import * as path from 'path'; -import { OutroKind, type TaskNotice } from '@lib/wizard-session'; +import { OutroKind, type TaskNotice } from '@lib/agent/progress'; import { POSTHOG_DOCS_URL, WIZARD_CONTACT_EMAIL } from '@lib/constants'; import { installSkillById, diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index 20961435b..b4f8bec8c 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -9,14 +9,11 @@ * that rebuilds today's session-driven behavior on top of this contract. */ -import type { - AdditionalFeature, - CloudRegion, - Credentials, - OutroData, - TaskNotice, - WizardSession, -} from '@lib/wizard-session'; +import type { WizardSession } from '@lib/wizard-session'; +import type { AdditionalFeature } from '@lib/constants'; +import type { CloudRegion } from '@utils/types'; +import type { Credentials } from '@lib/api'; +import type { OutroData, TaskNotice } from '@lib/agent/progress'; import type { PromptContext } from '@lib/agent/agent-prompt'; import type { PackageManagerDetector } from '@lib/detection/package-manager'; import type { ApiProject, ApiUser } from '@lib/api'; diff --git a/src/lib/api.ts b/src/lib/api.ts index 02bf1c04a..7fa1df2e7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2,6 +2,7 @@ import axios, { AxiosError } from 'axios'; import { z } from 'zod'; import { analytics } from '@utils/analytics'; import { WIZARD_USER_AGENT } from './constants'; +import type { HostResolution } from './host-resolution'; /** * User payload from `/api/users/@me/`. Schema typed for the fields the @@ -16,6 +17,30 @@ import { WIZARD_USER_AGENT } from './constants'; * Keep `distinct_id` required — analytics depends on it. Everything * else added here is nullish so partial responses don't fail parsing. */ +/** What a login (or a CI api key) resolves to: the wizard's access to one project. */ +export interface Credentials { + accessToken: string; + /** OAuth refresh token when the grant carried one; absent on CI api-key runs. */ + refreshToken?: string; + /** Epoch ms when `accessToken` expires — drives the pre-run refresh. */ + expiresAt?: number; + /** Minting OAuth client when it differs from the default login app (provisioning signups). */ + oauthClientId?: string; + projectApiKey: string; + /** Resolved at auth time and immutable thereafter — see {@link HostResolution}. */ + host: HostResolution; + projectId: number; + /** + * Requested OAuth scopes the grant came back without — deselected on the + * consent screen or clamped by the app's ceiling. Read when a run fails so + * the error can name the missing permission and the fix (re-run and grant + * it during the OAuth flow) instead of the generic report-a-bug line. + * Empty/absent on CI api-key runs, where there is no scope request to diff + * against. + */ + missingScopes?: readonly string[]; +} + export const ApiUserSchema = z .object({ // Identifiers diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a0162e708..1fd68c9c0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -105,6 +105,21 @@ export enum Integration { javascriptNode = 'javascript_node', } +/** Additional features the agent can integrate after the main setup */ +export enum AdditionalFeature { + LLM = 'llm', +} + +/** Human-readable labels for additional features (used in TUI progress) */ +export const ADDITIONAL_FEATURE_LABELS: Record = { + [AdditionalFeature.LLM]: 'AI observability', +}; + +/** Agent prompts for each additional feature, injected via the stop hook */ +export const ADDITIONAL_FEATURE_PROMPTS: Record = { + [AdditionalFeature.LLM]: `Now integrate AI observability with PostHog. Use the PostHog MCP server to find the appropriate AI observability skill, install it, and follow its workflow. PostHog basics are already installed. Update the setup report markdown file when complete with additions from this task. `, +}; + export interface Args { debug: boolean; integration: Integration; diff --git a/src/lib/wizard-ask-bridge.ts b/src/lib/wizard-ask-bridge.ts index 9dc974d60..290d18ee7 100644 --- a/src/lib/wizard-ask-bridge.ts +++ b/src/lib/wizard-ask-bridge.ts @@ -17,7 +17,7 @@ import type { AskAnswers, AskQuestion, PendingQuestion, -} from './wizard-session'; +} from './agent/progress'; export interface WizardAskRequest { questions: AskQuestion[]; diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 2fc028d59..bb3b8a621 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -11,35 +11,40 @@ */ import { POSTHOG_LOCAL_URL, resolveLocalDev } from './local-dev'; -import type { Harness, Integration, Sequence } from './constants'; +import { + AdditionalFeature, + ADDITIONAL_FEATURE_LABELS, + ADDITIONAL_FEATURE_PROMPTS, + type Harness, + type Integration, + type 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 { ApiUser, ApiProject } from './api'; -import type { HostResolution } from './host-resolution'; - -export interface Credentials { - accessToken: string; - /** OAuth refresh token when the grant carried one; absent on CI api-key runs. */ - refreshToken?: string; - /** Epoch ms when `accessToken` expires — drives the pre-run refresh. */ - expiresAt?: number; - /** Minting OAuth client when it differs from the default login app (provisioning signups). */ - oauthClientId?: string; - projectApiKey: string; - /** Resolved at auth time and immutable thereafter — see {@link HostResolution}. */ - host: HostResolution; - projectId: number; - /** - * Requested OAuth scopes the grant came back without — deselected on the - * consent screen or clamped by the app's ceiling. Read when a run fails so - * the error can name the missing permission and the fix (re-run and grant - * it during the OAuth flow) instead of the generic report-a-bug line. - * Empty/absent on CI api-key runs, where there is no scope request to diff - * against. - */ - missingScopes?: readonly string[]; -} +import type { ApiUser, ApiProject, Credentials } from './api'; +import type { CloudRegion } from '@utils/types'; +import { + OutroKind, + type AskAnswers, + type AskQuestion, + type OutroData, + type PendingQuestion, + type TaskNotice, +} from './agent/progress'; + +// These shapes moved to their owners; re-exported so every session reader +// keeps its import path. `Credentials` sits with the API types, the +// additional-feature enum with the other program enums in `./constants`, and +// the outro, question and task-notice shapes are the agent's contract. +export type { Credentials, CloudRegion }; +export { + AdditionalFeature, + ADDITIONAL_FEATURE_LABELS, + ADDITIONAL_FEATURE_PROMPTS, +}; +export { OutroKind }; +export type { AskAnswers, AskQuestion, OutroData, PendingQuestion, TaskNotice }; function parseProjectIdArg(value: string | undefined): number | undefined { if (value === undefined || value === '') return undefined; @@ -47,8 +52,6 @@ function parseProjectIdArg(value: string | undefined): number | undefined { return Number.isInteger(n) && n > 0 ? n : undefined; } -export type CloudRegion = 'us' | 'eu'; - /** Lifecycle phase of the main work (agent run, MCP install, etc.) */ export enum RunPhase { /** Still gathering input (intro, setup screens) */ @@ -74,21 +77,6 @@ export enum ScanConsent { Declined = 'declined', } -/** Additional features the agent can integrate after the main setup */ -export enum AdditionalFeature { - LLM = 'llm', -} - -/** Human-readable labels for additional features (used in TUI progress) */ -export const ADDITIONAL_FEATURE_LABELS: Record = { - [AdditionalFeature.LLM]: 'AI observability', -}; - -/** Agent prompts for each additional feature, injected via the stop hook */ -export const ADDITIONAL_FEATURE_PROMPTS: Record = { - [AdditionalFeature.LLM]: `Now integrate AI observability with PostHog. Use the PostHog MCP server to find the appropriate AI observability skill, install it, and follow its workflow. PostHog basics are already installed. Update the setup report markdown file when complete with additions from this task. `, -}; - /** Outcome of the MCP server installation step */ export enum McpOutcome { NoClients = 'no_clients', @@ -97,120 +85,6 @@ export enum McpOutcome { Failed = 'failed', } -/** Outcome kind for the outro screen */ -export enum OutroKind { - Success = 'success', - Error = 'error', - Cancel = 'cancel', -} - -export interface OutroData { - kind: OutroKind; - /** Main headline (green check for Success, red X for Error, etc.) */ - message?: string; - /** Free-form body text shown under the headline. Use \n for paragraph breaks. */ - body?: string; - /** Success-only: bulleted list of "what the agent did" */ - changes?: string[]; - /** - * Success-only: a prominent, labeled link to where the user should go - * next (e.g. an inbox the program just configured). Rendered right under - * the headline and shown verbatim — no UTM tagging — so the URL stays - * clean and copy-pasteable. Set per-program in buildOutroData. - */ - primaryLink?: { label: string; url: string }; - /** - * Success-only: a short "what to do next" checklist with its own heading, - * rendered as a bulleted list. Distinct from `changes`, which recaps what - * the agent already did. - */ - nextSteps?: { heading: string; items: string[] }; - docsUrl?: string; - continueUrl?: string; - /** Report file the agent wrote (e.g. "posthog-setup-report.md") */ - reportFile?: string; - /** Stable machine-readable error code from the error catalog (@lib/errors). */ - errorCode?: import('@lib/errors').ErrorCode; - /** Structured context for the error code; safe for telemetry payloads. */ - errorDetail?: Record; - /** PostHog dashboard URL the program created on the user's behalf. */ - dashboardUrl?: string; - /** PostHog notebook URL the program uploaded the report to. */ - notebookUrl?: string; - /** - * Copy-paste prompt the operator hands to their coding agent to finish the - * job (work the report's checklist). Printed to the terminal's main buffer on - * exit (see getExitLine in start-tui.ts) — the TUI's alternate screen is wiped - * on exit, so the scrollback line is where it survives and can be - * triple-click-selected. Set per-program in buildOutroData. - */ - handoffPrompt?: string; -} - -/** A single question rendered by the WizardAsk overlay. */ -export interface AskQuestion { - /** Key for the response map */ - id: string; - prompt: string; - /** text = single-line free input; single/multi = picker */ - kind: 'single' | 'multi' | 'text'; - /** Required for `single` and `multi`. Ignored for `text`. */ - options?: { label: string; value: string; description?: string }[]; - /** Defaults to true */ - required?: boolean; - /** - * Only meaningful for kind='text'. When true, the wizard-tools `wizard_ask` - * tool stores the user's answer in the session secret vault and returns - * `{ secretRef }` to the agent instead of the plain string — so the value - * never enters the LLM conversation. The TUI masks the input as it is typed - * (see `shouldMaskAnswer`). See `secret-vault.ts`. - */ - sensitive?: boolean; -} - -/** - * Copy for a modal shown before an optional step runs, so the user can decline - * it. The program that owns the step supplies the words; the runner and the - * screen only carry them. - */ -export interface TaskNotice { - title: string; - /** Paragraphs, in order. */ - body: string[]; - /** Optional highlighted list, e.g. what was detected. */ - items?: string[]; - docsLabel?: string; - docsUrl?: string; - confirmLabel: string; - cancelLabel: string; - prompt: string; -} - -/** Map of question id → answer (string for single/text, string[] for multi). */ -export type AskAnswers = Record; - -/** A pending wizard_ask request held by the store. */ -export interface PendingQuestion { - id: string; - questions: AskQuestion[]; - /** - * UTC ISO 8601 timestamp of when the ask was created. Published on the - * task stream as `pending_input.asked_at` so the web app can age the - * prompt; stable across pushes for the lifetime of one ask. - */ - askedAt?: string; - /** Skill id of the caller. Set by the wizard from session.skillId. */ - source: string; - /** - * When true, the ask overlay renders standalone URLs in prompt text as - * OSC 8 hyperlinks and copies a lone URL to the clipboard. Opt-in per - * program (set from `ProgramRun.richLinks` via the ask bridge); defaults - * to false so existing flows render prompts exactly as before. See - * `LinkText` / `link-helpers`. - */ - richLinks?: boolean; -} - /** * PostHog dashboard URL emitted by the agent during a program run. * Populated via the `[DASHBOARD_URL]` text marker in agent assistant messages From 53bddf83a7d8269a95eba1ecb79c7349afe85e24 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:21:06 -0400 Subject: [PATCH 12/61] refactor(programs): split ProgramRun into the agent definition and the program hooks The agent's RunConfig.run is now AgentRunDefinition: prompt, skill, tools, copy, ask policy. ProgramRun extends it in src/lib/programs/program-run.ts with the three session-taking completion hooks (postRun, buildOutroData, buildOutroNextSteps) that only the legacy adapter calls. Programs import ProgramRun from there; the agent no longer references WizardSession anywhere. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../architecture/known-violations.json | 8 +--- src/lib/agent/__tests__/agent-prompt.test.ts | 6 ++- src/lib/agent/agent-interface.ts | 6 +-- src/lib/agent/agent-prompt.ts | 7 ++- src/lib/agent/agent-runner.ts | 2 +- src/lib/agent/progress.ts | 2 +- src/lib/agent/runner/index.ts | 2 +- src/lib/agent/runner/sequence/README.md | 4 +- src/lib/agent/runner/sequence/linear.ts | 2 +- src/lib/agent/runner/shared/types.ts | 47 ++++--------------- .../programs/__tests__/agent-skill.test.ts | 2 +- .../programs/__tests__/error-tracking.test.ts | 2 +- .../__tests__/metrics-program.test.ts | 2 +- src/lib/programs/agent-skill/index.ts | 3 +- src/lib/programs/audit/index.ts | 2 +- .../index.ts | 2 +- src/lib/programs/error-tracking/index.ts | 2 +- src/lib/programs/events-audit/index.ts | 2 +- src/lib/programs/posthog-integration/index.ts | 2 +- src/lib/programs/program-run.ts | 39 +++++++++++++++ src/lib/programs/program-step.ts | 2 +- src/lib/programs/run-agent-legacy.ts | 2 +- src/lib/programs/self-driving/index.ts | 2 +- src/lib/programs/warehouse-source/index.ts | 2 +- 24 files changed, 80 insertions(+), 72 deletions(-) create mode 100644 src/lib/programs/program-run.ts diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 57c17d063..86b38fad1 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -9,17 +9,13 @@ "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", @@ -28,7 +24,7 @@ "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/agent-runner.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-run.ts -> src/lib/agent/runner/index.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", @@ -42,14 +38,12 @@ "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/yara-hooks.ts", "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", diff --git a/src/lib/agent/__tests__/agent-prompt.test.ts b/src/lib/agent/__tests__/agent-prompt.test.ts index 2ee6bfdb9..e6bad880c 100644 --- a/src/lib/agent/__tests__/agent-prompt.test.ts +++ b/src/lib/agent/__tests__/agent-prompt.test.ts @@ -1,8 +1,10 @@ import { assemblePrompt, type PromptContext } from '@lib/agent/agent-prompt'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { AgentRunDefinition } from '@lib/agent/runner'; import { HostResolution } from '@lib/host-resolution'; -function makeRunDef(overrides: Partial = {}): ProgramRun { +function makeRunDef( + overrides: Partial = {}, +): AgentRunDefinition { return { integrationLabel: 'test', spinnerMessage: 'Working...', diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 3e7a17ad4..283420ad6 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -729,12 +729,12 @@ export async function runAgent( abortCases?: readonly AbortCaseMatcher[]; /** * Emit a `wizard: step` event on each agent task transition. Threaded from - * `ProgramRun.trackStepProgress`; defaults off for every other caller. + * `AgentRunDefinition.trackStepProgress`; defaults off for every other caller. */ emitStepEvents?: boolean; /** * Maps an agent-authored step label to a stable `step_key`. Threaded from - * `ProgramRun.resolveStepKey`; absent for programs that don't define one. + * `AgentRunDefinition.resolveStepKey`; absent for programs that don't define one. */ resolveStepKey?: (stepName: string | undefined) => string | undefined; /** Request the end-of-run reflection remark. Defaults to true. */ @@ -1783,7 +1783,7 @@ function handleSDKMessage( // agent's own TaskCreate/TaskUpdate rendering so it does not clobber the queue. suppressTaskRender = false, // Opt-in per-step analytics, threaded from runAgent's `emitStepEvents` - // (ProgramRun.trackStepProgress). Off for every program that doesn't opt in. + // (AgentRunDefinition.trackStepProgress). Off for every program that doesn't opt in. emitStepEvents = false, // Program-supplied label -> stable key mapping for the same events. resolveStepKey?: (stepName: string | undefined) => string | undefined, diff --git a/src/lib/agent/agent-prompt.ts b/src/lib/agent/agent-prompt.ts index 0427fc4f5..4987adcfb 100644 --- a/src/lib/agent/agent-prompt.ts +++ b/src/lib/agent/agent-prompt.ts @@ -7,7 +7,7 @@ * 3. Skill prompt — "follow SKILL.md" instructions (if a skill was installed) */ -import type { ProgramRun } from './agent-runner.js'; +import type { AgentRunDefinition } from './runner/shared/types'; import type { HostResolution } from '@lib/host-resolution'; /** @@ -62,7 +62,10 @@ Important: You must read a file immediately before attempting to write it, even /** * Assemble the final agent prompt from the program's run config. */ -export function assemblePrompt(runDef: ProgramRun, ctx: PromptContext): string { +export function assemblePrompt( + runDef: AgentRunDefinition, + ctx: PromptContext, +): string { const parts: string[] = []; // Always include the default project prompt diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts index 2850afcc1..7ffac5145 100644 --- a/src/lib/agent/agent-runner.ts +++ b/src/lib/agent/agent-runner.ts @@ -8,7 +8,7 @@ export { runAgent, shouldDisableAsk, - type ProgramRun, + type AgentRunDefinition, type BootstrapResult, type AbortCase, type PromptContext, diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index f99962a4a..eb5386ff4 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -120,7 +120,7 @@ export interface PendingQuestion { /** * When true, the ask overlay renders standalone URLs in prompt text as * OSC 8 hyperlinks and copies a lone URL to the clipboard. Opt-in per - * program (set from `ProgramRun.richLinks` via the ask bridge); defaults + * program (set from `AgentRunDefinition.richLinks` via the ask bridge); defaults * to false so existing flows render prompts exactly as before. See * `LinkText` / `link-helpers`. */ diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index 10ea0bdf8..70f213775 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -43,7 +43,7 @@ export type { AgentFailure, BootstrapResult, Credentials, - ProgramRun, + AgentRunDefinition, PromptContext, ResolvedBinding, RunAgentOptions, diff --git a/src/lib/agent/runner/sequence/README.md b/src/lib/agent/runner/sequence/README.md index 038fdeb01..d50ce96ad 100644 --- a/src/lib/agent/runner/sequence/README.md +++ b/src/lib/agent/runner/sequence/README.md @@ -12,8 +12,8 @@ prompt assembly, error routing, post-run work, and outro construction. Retain it for very simple tasks and legacy support. Its context is subject to the harness's compaction behavior. -`ProgramRun.customPrompt`, `abortCases`, `postRun`, and `buildOutroData` are -linear hooks. The orchestrator does not invoke them. Composed program sub-runs +`AgentRunDefinition.customPrompt`, `abortCases`, and the program's `postRun` +and `buildOutroData` hooks are linear hooks. The orchestrator does not invoke them. Composed program sub-runs are also clamped to linear because an orchestrator owns its full lifecycle and cannot nest through the composition seam. diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index 3f75d0c5e..8fe2a8df9 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -1,7 +1,7 @@ /** * The linear pipeline. Single execution path for all non-orchestrator programs, * both skill-based (revenue analytics) and framework-based (core integration). - * The `ProgramRun` controls what varies between them; `RunConfig` + * The `AgentRunDefinition` controls what varies between them; `RunConfig` * carries the program-level static metadata (tool allow/disallow lists, etc.). * * Reports through `emit`, asks through `interaction`, and returns a decided diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index b4f8bec8c..82df5779b 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -9,7 +9,6 @@ * that rebuilds today's session-driven behavior on top of this contract. */ -import type { WizardSession } from '@lib/wizard-session'; import type { AdditionalFeature } from '@lib/constants'; import type { CloudRegion } from '@utils/types'; import type { Credentials } from '@lib/api'; @@ -39,17 +38,14 @@ export interface AbortCase { } /** - * Unified agent run configuration. + * What varies between agent runs: the prompt, the skill, the tools, the copy. * - * 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`. - * - * The three session-taking hooks at the end are the caller's: the agent never - * calls them. `run-agent-legacy.ts` binds them and hands the agent - * `RunConfig.hooks` instead. + * Every program provides one of these as `RunConfig.run`. The runner assembles + * the final prompt from `customPrompt` + `skillId`. Programs extend it with + * their session-taking completion hooks in `src/lib/programs/program-run.ts`; + * the caller binds those and hands the agent `RunConfig.hooks` instead. */ -export interface ProgramRun { +export interface AgentRunDefinition { /** Analytics label (e.g. 'revenue-analytics-setup', 'nextjs') */ integrationLabel: string; /** Skill ID to pre-install. Omit for agent-driven skill discovery. */ @@ -69,33 +65,6 @@ export interface ProgramRun { 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. @@ -176,8 +145,8 @@ export interface ResolvedBinding { export interface RunConfig { /** Program id: gateway spend pin, analytics label, commandments axis. */ programId: string; - /** The program's run definition. Its session-taking hooks are the caller's, see `hooks`. */ - run: ProgramRun; + /** The run definition. A program's session-taking hooks are the caller's, see `hooks`. */ + run: AgentRunDefinition; /** A composed sub-run leaves the terminal outro to its host. */ composed: boolean; /** Run-level sequence, harness and model. */ diff --git a/src/lib/programs/__tests__/agent-skill.test.ts b/src/lib/programs/__tests__/agent-skill.test.ts index 132586e1d..b836f0486 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/programs/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..5ecef5279 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/programs/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..46f381471 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/programs/program-run'; import { metricsCommand } from '../../../commands/metrics'; diff --git a/src/lib/programs/agent-skill/index.ts b/src/lib/programs/agent-skill/index.ts index dbd207ae0..ace6ab19d 100644 --- a/src/lib/programs/agent-skill/index.ts +++ b/src/lib/programs/agent-skill/index.ts @@ -20,7 +20,8 @@ */ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun, AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/programs/program-run'; import { AGENT_SKILL_STEPS } from './steps.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/audit/index.ts b/src/lib/programs/audit/index.ts index fc5f60297..261fbaefc 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/programs/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/index.ts b/src/lib/programs/error-tracking-upload-source-maps/index.ts index c79f245e2..afdc17007 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/programs/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/index.ts b/src/lib/programs/error-tracking/index.ts index 2fd6a7cb1..68e7986b5 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/programs/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..4cfa397f0 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/programs/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/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index c7ea2560a..9fc7aab8e 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -1,6 +1,6 @@ import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/programs/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'; diff --git a/src/lib/programs/program-run.ts b/src/lib/programs/program-run.ts new file mode 100644 index 000000000..35a3ea90f --- /dev/null +++ b/src/lib/programs/program-run.ts @@ -0,0 +1,39 @@ +/** + * A program's run definition: the agent's `AgentRunDefinition` plus the + * completion hooks that read the session. The agent never calls these — + * `run-agent-legacy.ts` binds them to the run's credentials and hands the + * agent `RunConfig.hooks`. + */ + +import type { AgentRunDefinition } from '@lib/agent/runner'; +import type { Credentials, WizardSession } from '@lib/wizard-session'; + +export interface ProgramRun extends AgentRunDefinition { + /** 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; +} diff --git a/src/lib/programs/program-step.ts b/src/lib/programs/program-step.ts index 81e55a655..69d0d7cfd 100644 --- a/src/lib/programs/program-step.ts +++ b/src/lib/programs/program-step.ts @@ -4,7 +4,7 @@ import type { TaskNotice, } from '@lib/wizard-session'; import type { WizardReadinessResult } from '@lib/health-checks/readiness'; -import type { ProgramRun } from '@lib/agent/agent-runner'; +import type { ProgramRun } from '@lib/programs/program-run'; import type { Integration } from '@lib/constants'; import type { FrameworkConfig } from '@lib/framework-config'; import type { ContentBlock } from '@ui/tui/primitives/index'; diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index d8315cb88..81bf0a686 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -21,12 +21,12 @@ import { runAgent, RunOutcome, resolveBinding, - type ProgramRun, type RunConfig, type RunInput, type SwitchboardCtx, } from '@lib/agent/runner'; import type { ProgramBinding } from '@lib/agent/runner/switchboard'; +import type { ProgramRun } from './program-run'; import { buildRunTags } from '@lib/agent/agent-interface'; import { backupAndFixClaudeSettings, diff --git a/src/lib/programs/self-driving/index.ts b/src/lib/programs/self-driving/index.ts index b507cc18e..38a0ad930 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/programs/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/warehouse-source/index.ts b/src/lib/programs/warehouse-source/index.ts index 059e240a0..4b31f3851 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/programs/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'; From 54f8e4be478ebe54ba79c3a8582cb9d66d25e42f Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:22:44 -0400 Subject: [PATCH 13/61] refactor(programs): move authenticate and the token refresh to programs Both read and write the session, drive the OAuth flow through the UI and take a ProgramId. Plan section 4.5: programs own authentication and refresh. File and test move unchanged; five importers repoint. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- scripts/tui-host.no-jest.ts | 2 +- src/__tests__/architecture/known-violations.json | 2 -- src/lib/detection/__tests__/project-scope.test.ts | 4 ++-- src/lib/detection/project-scope.ts | 2 +- .../__tests__/refresh-access-token-if-needed.test.ts | 0 src/lib/programs/__tests__/run-agent-legacy.test.ts | 4 ++-- src/lib/{agent/runner/shared => programs}/authenticate.ts | 0 src/lib/programs/run-agent-legacy.ts | 5 +---- src/lib/runners/run-wizard.ts | 2 +- 9 files changed, 8 insertions(+), 13 deletions(-) rename src/lib/{agent/runner/shared => programs}/__tests__/refresh-access-token-if-needed.test.ts (100%) rename src/lib/{agent/runner/shared => programs}/authenticate.ts (100%) diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index d20bfc8cb..61ca921bf 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -30,7 +30,7 @@ import { configureGatewayFromCIEnvironment } from '@lib/gateway-session'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; import { getAuditChecks } from '@lib/programs/audit/types'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { logToFile } from '@utils/debug'; import { join } from 'path'; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 86b38fad1..d270a05bb 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -3,7 +3,6 @@ "src/commands/factories/family-picker.tsx -> src/commands/command.ts", "src/env.ts -> src/lib/headless-mode.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", @@ -34,7 +33,6 @@ "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/agent-interface.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/claude-settings.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/index.ts", - "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/shared/authenticate.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/yara-hooks.ts", "src/lib/programs/self-driving/detect.ts -> src/lib/agent/agent-runner.ts", diff --git a/src/lib/detection/__tests__/project-scope.test.ts b/src/lib/detection/__tests__/project-scope.test.ts index 46aedaf04..18f9173dc 100644 --- a/src/lib/detection/__tests__/project-scope.test.ts +++ b/src/lib/detection/__tests__/project-scope.test.ts @@ -10,12 +10,12 @@ import { AGENTIC_DETECTION_TIMEOUT_MS, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, } from '@lib/constants'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { buildSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; // Mock only the two network edges of scopeInstallDirToProject; everything else runs real. -vi.mock('@lib/agent/runner/shared/authenticate', () => ({ +vi.mock('@lib/programs/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), })); vi.mock('@lib/detection/agentic', async (importOriginal) => ({ diff --git a/src/lib/detection/project-scope.ts b/src/lib/detection/project-scope.ts index 73279b075..da4ffaf0a 100644 --- a/src/lib/detection/project-scope.ts +++ b/src/lib/detection/project-scope.ts @@ -8,7 +8,7 @@ import { type DetectEvent, type DetectTarget, } from './agentic.js'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { AGENTIC_DETECTION_TIMEOUT_MS, diff --git a/src/lib/agent/runner/shared/__tests__/refresh-access-token-if-needed.test.ts b/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts similarity index 100% rename from src/lib/agent/runner/shared/__tests__/refresh-access-token-if-needed.test.ts rename to src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index 6a0b9b1c4..c2ad3dead 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -1,5 +1,5 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { runProgramAgent } from '../run-agent-legacy'; import { runAgent, RunOutcome, type RunResult } from '@lib/agent/runner'; import { Harness, Sequence } from '@lib/constants'; @@ -54,7 +54,7 @@ vi.mock('@lib/agent/runner', async (original) => ({ ...(await original()), runAgent: vi.fn(), })); -vi.mock('@lib/agent/runner/shared/authenticate', () => ({ +vi.mock('@lib/programs/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), refreshAccessTokenIfNeeded: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/lib/agent/runner/shared/authenticate.ts b/src/lib/programs/authenticate.ts similarity index 100% rename from src/lib/agent/runner/shared/authenticate.ts rename to src/lib/programs/authenticate.ts diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index 81bf0a686..87b8f8e36 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -55,10 +55,7 @@ import { } from '@lib/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { postAuthGateSteps, type ProgramConfig } from './program-step'; -import { - authenticate, - refreshAccessTokenIfNeeded, -} from '@lib/agent/runner/shared/authenticate'; +import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; import { maybeStampAiSdkDetected } from './posthog-integration/detect'; import { startAuditLedgerWatcher } from './audit/ledger-watcher'; diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index 42b257cd5..a1f5c5390 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,7 +1,7 @@ import { VERSION } from '@lib/version'; import { logToFile, getLogFilePath } from '@utils/debug'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; -import { authenticate } from '@lib/agent/runner/shared/authenticate'; +import { authenticate } from '@lib/programs/authenticate'; import { getProgramConfig } from '@lib/programs/program-registry'; import { getAuditChecks } from '@lib/programs/audit/types'; import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; From 85ed50e4641c9588bc90a27cda1104b8473f85d6 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:25:12 -0400 Subject: [PATCH 14/61] refactor(shared): name program documents and the audit ledger outside programs yara-hooks and the audit ledger tools imported filename constants and the AuditCheck shape from three programs. The document names now live in @lib/constants, the ledger contract (file, check shape, read-side coercion) in the new leaf @lib/audit-ledger, and each program re-exports its own, so the audit views and the ledger watcher are unchanged. Nothing agent-side imports @lib/programs at runtime any more. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/agent/runner/harness/pi/tools.ts | 7 +++- src/lib/audit-ledger.ts | 37 +++++++++++++++++ src/lib/constants.ts | 12 ++++++ src/lib/programs/audit/types.ts | 40 +++++-------------- src/lib/programs/events-audit/constants.ts | 18 ++++----- .../programs/posthog-integration/constants.ts | 11 ++--- src/lib/wizard-tools/mcp.ts | 2 +- src/lib/wizard-tools/tools.ts | 2 +- src/lib/yara-hooks.ts | 22 +++++----- 9 files changed, 88 insertions(+), 63 deletions(-) create mode 100644 src/lib/audit-ledger.ts diff --git a/src/lib/agent/runner/harness/pi/tools.ts b/src/lib/agent/runner/harness/pi/tools.ts index b194cc79a..06121652c 100644 --- a/src/lib/agent/runner/harness/pi/tools.ts +++ b/src/lib/agent/runner/harness/pi/tools.ts @@ -62,8 +62,11 @@ import { publishHandoff, } from '@lib/wizard-tools/handoff'; import { createSecretVault } from '@lib/secret-vault'; -import { AUDIT_CHECKS_FILE } from '@lib/programs/audit/types'; -import type { AuditCheck, AuditStatus } from '@lib/programs/audit/types'; +import { + AUDIT_CHECKS_FILE, + type AuditCheck, + type AuditStatus, +} from '@lib/audit-ledger'; import { makeMutex } from '@utils/atomic-ledger'; import { withMode } from './index'; import { diff --git a/src/lib/audit-ledger.ts b/src/lib/audit-ledger.ts new file mode 100644 index 000000000..74228ef09 --- /dev/null +++ b/src/lib/audit-ledger.ts @@ -0,0 +1,37 @@ +/** + * The audit ledger's contract: the file the audit tools write into the user's + * project, the check shape inside it, and the read-side coercion. Shared by + * the agent's ledger tools (both harnesses), the scanner's documentation + * allowlist and the audit program that watches the file. + */ + +export type AuditStatus = + | 'pending' + | 'pass' + | 'error' + | 'warning' + | 'suggestion'; + +export interface AuditCheck { + id: string; + area: string; + label: string; + status: AuditStatus; + file?: string; + details?: string; +} + +export const AUDIT_CHECKS_FILE = '.posthog-audit-checks.json'; +export const AUDIT_REPORT_FILE = 'posthog-audit-report.md'; + +/** + * Read the audit checks ledger off disk. Validation lives at write time — + * every writer (`audit_seed_checks` / `audit_add_checks` / `audit_resolve_checks` + * MCP tools, `seedAuditLedger`) zod-parses entries before the atomic write, + * so by the time the file watcher fires we trust the shape and only guard + * against the file not being a JSON array (corrupted / hand-edited / not yet + * seeded). + */ +export function coerceAuditChecks(parsed: unknown): AuditCheck[] { + return Array.isArray(parsed) ? (parsed as AuditCheck[]) : []; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1fd68c9c0..c055778ce 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -120,6 +120,18 @@ export const ADDITIONAL_FEATURE_PROMPTS: Record = { [AdditionalFeature.LLM]: `Now integrate AI observability with PostHog. Use the PostHog MCP server to find the appropriate AI observability skill, install it, and follow its workflow. PostHog basics are already installed. Update the setup report markdown file when complete with additions from this task. `, }; +// ── Documents the wizard's programs write into the user's project ──── +// Named here so the scanner's documentation allowlist can list them without +// importing a program; each program re-exports its own. +/** The events-audit report. */ +export const EVENTS_AUDIT_REPORT_FILE = 'posthog-events-audit-report.md'; +export const EVENT_INVENTORY_FILE = '.posthog-events-inventory.json'; +/** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */ +export const EVENT_INVENTORY_PART_PATTERN = + /^\.posthog-events-inventory\.part-\d+\.json$/; +/** The integration program's event plan. */ +export const EVENT_PLAN_FILE = '.posthog-events.json'; + export interface Args { debug: boolean; integration: Integration; diff --git a/src/lib/programs/audit/types.ts b/src/lib/programs/audit/types.ts index dd858a709..0a705ce24 100644 --- a/src/lib/programs/audit/types.ts +++ b/src/lib/programs/audit/types.ts @@ -1,20 +1,16 @@ import type { WizardSession } from '@lib/wizard-session'; +import { + AUDIT_CHECKS_FILE, + AUDIT_REPORT_FILE, + coerceAuditChecks, + type AuditCheck, + type AuditStatus, +} from '@lib/audit-ledger'; -export type AuditStatus = - | 'pending' - | 'pass' - | 'error' - | 'warning' - | 'suggestion'; - -export interface AuditCheck { - id: string; - area: string; - label: string; - status: AuditStatus; - file?: string; - details?: string; -} +// The ledger contract lives in `@lib/audit-ledger`; re-exported so the audit +// views and the ledger watcher keep their import path. +export { AUDIT_CHECKS_FILE, AUDIT_REPORT_FILE, coerceAuditChecks }; +export type { AuditCheck, AuditStatus }; export interface AuditSeverityStyle { glyph: string; @@ -30,23 +26,9 @@ export const AUDIT_SEVERITY_STYLE: Record = { suggestion: { glyph: '•', color: 'cyan' }, }; -export const AUDIT_CHECKS_FILE = '.posthog-audit-checks.json'; -export const AUDIT_REPORT_FILE = 'posthog-audit-report.md'; export const AUDIT_CHECKS_KEY = 'auditChecks'; export function getAuditChecks(session: WizardSession): AuditCheck[] { const raw = session.frameworkContext[AUDIT_CHECKS_KEY]; return Array.isArray(raw) ? (raw as AuditCheck[]) : []; } - -/** - * Read the audit checks ledger off disk. Validation lives at write time — - * every writer (`audit_seed_checks` / `audit_add_checks` / `audit_resolve_checks` - * MCP tools, `seedAuditLedger`) zod-parses entries before the atomic write, - * so by the time the file watcher fires we trust the shape and only guard - * against the file not being a JSON array (corrupted / hand-edited / not yet - * seeded). - */ -export function coerceAuditChecks(parsed: unknown): AuditCheck[] { - return Array.isArray(parsed) ? (parsed as AuditCheck[]) : []; -} diff --git a/src/lib/programs/events-audit/constants.ts b/src/lib/programs/events-audit/constants.ts index 52842d0b5..a70c343df 100644 --- a/src/lib/programs/events-audit/constants.ts +++ b/src/lib/programs/events-audit/constants.ts @@ -1,13 +1,11 @@ /** - * Leaf-level constants for the events-audit program. - * - * Kept separate from `index.ts` so files like `yara-hooks.ts` can import - * the filename constants without dragging in `index.ts`'s heavier imports - * (agent-runner, audit/seed, etc.) — which can create import cycles. + * Leaf-level constants for the events-audit program. The document names live + * in `@lib/constants` so infrastructure (the scanner's allowlist) can name + * them without importing this program. */ -export const SETUP_REPORT_FILE = 'posthog-events-audit-report.md'; -export const EVENT_INVENTORY_FILE = '.posthog-events-inventory.json'; -/** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */ -export const EVENT_INVENTORY_PART_PATTERN = - /^\.posthog-events-inventory\.part-\d+\.json$/; +export { + EVENTS_AUDIT_REPORT_FILE as SETUP_REPORT_FILE, + EVENT_INVENTORY_FILE, + EVENT_INVENTORY_PART_PATTERN, +} from '@lib/constants'; diff --git a/src/lib/programs/posthog-integration/constants.ts b/src/lib/programs/posthog-integration/constants.ts index 1f854850e..a0bb8d34b 100644 --- a/src/lib/programs/posthog-integration/constants.ts +++ b/src/lib/programs/posthog-integration/constants.ts @@ -1,10 +1,7 @@ /** - * Leaf-level constants for the posthog-integration program. - * - * Kept separate from `index.ts` so files like `yara-hooks.ts` can import - * the filename constants without dragging in `index.ts`'s heavier imports - * (agent-interface, framework-config, etc.) — which would create an import - * cycle through agent-interface → yara-hooks. + * Leaf-level constants for the posthog-integration program. The event plan's + * name lives in `@lib/constants` so infrastructure (the scanner's allowlist) + * can name it without importing this program. */ -export const EVENT_PLAN_FILE = '.posthog-events.json'; +export { EVENT_PLAN_FILE } from '@lib/constants'; diff --git a/src/lib/wizard-tools/mcp.ts b/src/lib/wizard-tools/mcp.ts index 4ad43e235..15fafd432 100644 --- a/src/lib/wizard-tools/mcp.ts +++ b/src/lib/wizard-tools/mcp.ts @@ -19,7 +19,7 @@ import { AUDIT_CHECKS_FILE, type AuditCheck, type AuditStatus, -} from '../programs/audit/types'; +} from '../audit-ledger'; import { type WizardAskBridge, isFullyCancelled } from '../wizard-ask-bridge'; import { PUBLISH_HANDOFF_CONTENT_DESCRIPTION, diff --git a/src/lib/wizard-tools/tools.ts b/src/lib/wizard-tools/tools.ts index 06b35c55c..134de05f5 100644 --- a/src/lib/wizard-tools/tools.ts +++ b/src/lib/wizard-tools/tools.ts @@ -28,7 +28,7 @@ import { coerceAuditChecks, type AuditCheck, type AuditStatus, -} from '../programs/audit/types'; +} from '../audit-ledger'; import { CANCELLED_SENTINEL } from '../wizard-ask-bridge'; import type { SecretVault } from '../secret-vault'; import { fetchWithRetry, type RetryOpts } from '../fetch-retry'; diff --git a/src/lib/yara-hooks.ts b/src/lib/yara-hooks.ts index 3ec2cb266..cd3d052f0 100644 --- a/src/lib/yara-hooks.ts +++ b/src/lib/yara-hooks.ts @@ -45,22 +45,18 @@ import { } from './yara-policy'; import type { ScanAction, ScanContext } from './yara-policy'; import { WIZARD_YARA_REPORT_FILE } from '@utils/paths'; -// TODO(wizard#594): invert this dependency. -// L2 infra (yara-hooks) imports product-specific filename constants from -// individual programs. The leaf `constants.ts` modules break the *import* -// cycle but don't fix the *layering* concern: this file knowing about -// `events-audit`, `posthog-integration`, and `audit` violates "product -// knowledge never enters infrastructure code." Proper fix is inverted — -// programs declare their own doc paths, the hooks read from a generic -// registry. For now: every new program emitting a PII-shaped report has -// to be added here. Land that cleanup before adding a fourth entry. +// TODO(wizard#594): invert this dependency. The document names are shared +// constants now, so nothing here imports a program, but this file still knows +// which programs write PII-shaped reports: every new one has to be added. +// Proper fix is inverted — programs declare their own doc paths and the hooks +// read a generic registry. Land that before adding a fourth entry. import { - SETUP_REPORT_FILE as EVENTS_AUDIT_REPORT_FILE, + EVENTS_AUDIT_REPORT_FILE, EVENT_INVENTORY_FILE, EVENT_INVENTORY_PART_PATTERN, -} from '@lib/programs/events-audit/constants'; -import { AUDIT_REPORT_FILE } from '@lib/programs/audit/types'; -import { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/constants'; + EVENT_PLAN_FILE, +} from '@lib/constants'; +import { AUDIT_REPORT_FILE } from '@lib/audit-ledger'; // ─── Warlock module accessor ───────────────────────────────────── // Warlock is ESM-only and lazily inits its WASM engine + compiles rules on the From a88c581611ae8367cb86df6a2e9132ee090e471e Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:27:02 -0400 Subject: [PATCH 15/61] refactor(shared): move the Node package-manager detector next to its data PackageManagerDetector, PackageManagerInfo, DetectedPackageManager and detectNodePackageManagers move into @utils/package-manager, which already owns the lockfile detection they wrap. src/lib/detection/package-manager re-exports them, so every framework config is unchanged; the agent's two harnesses and the tools server now import the detector contract without importing detection. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/agent/agent-interface.ts | 2 +- .../agent/runner/harness/anthropic/index.ts | 2 +- src/lib/agent/runner/harness/pi/tools.ts | 2 +- src/lib/agent/runner/shared/types.ts | 2 +- src/lib/detection/package-manager.ts | 92 ++++--------------- src/lib/wizard-tools/mcp.ts | 2 +- src/utils/package-manager.ts | 67 ++++++++++++++ 7 files changed, 91 insertions(+), 78 deletions(-) diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 283420ad6..d4e9a3bcd 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -48,7 +48,7 @@ import { createTriageLLMProvider } from './triage-provider'; import type { LLMProvider } from '@posthog/warlock'; import { assembleCommandments } from './runner/switchboard/commandments'; import { classifyToolToStage } from './agent-phase'; -import type { PackageManagerDetector } from '@lib/detection/package-manager'; +import type { PackageManagerDetector } from '@utils/package-manager'; import { AgentSignals, AgentErrorType, diff --git a/src/lib/agent/runner/harness/anthropic/index.ts b/src/lib/agent/runner/harness/anthropic/index.ts index 660c5808e..1b967a662 100644 --- a/src/lib/agent/runner/harness/anthropic/index.ts +++ b/src/lib/agent/runner/harness/anthropic/index.ts @@ -7,7 +7,7 @@ import { } from '@lib/agent/agent-interface'; import { createAioCapture } from '@lib/agent/aio-capture'; import { getLogFilePath, logToFile } from '@utils/debug'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import { detectNodePackageManagers } from '@utils/package-manager'; import { runOptions } from '@lib/agent/runner/shared/bootstrap'; import { createEmitLog } from '@lib/agent/runner/shared/progress-collector'; import type { diff --git a/src/lib/agent/runner/harness/pi/tools.ts b/src/lib/agent/runner/harness/pi/tools.ts index 06121652c..360cf0df1 100644 --- a/src/lib/agent/runner/harness/pi/tools.ts +++ b/src/lib/agent/runner/harness/pi/tools.ts @@ -72,7 +72,7 @@ import { withMode } from './index'; import { detectNodePackageManagers, type PackageManagerDetector, -} from '@lib/detection/package-manager'; +} from '@utils/package-manager'; function text(s: string): { content: [{ type: 'text'; text: string }]; diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index 82df5779b..db73f8429 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -14,7 +14,7 @@ import type { CloudRegion } from '@utils/types'; import type { Credentials } from '@lib/api'; import type { OutroData, TaskNotice } from '@lib/agent/progress'; import type { PromptContext } from '@lib/agent/agent-prompt'; -import type { PackageManagerDetector } from '@lib/detection/package-manager'; +import type { PackageManagerDetector } from '@utils/package-manager'; import type { ApiProject, ApiUser } from '@lib/api'; import type { Harness, Integration, Sequence } from '@lib/constants'; import type { ErrorCode } from '@lib/errors'; diff --git a/src/lib/detection/package-manager.ts b/src/lib/detection/package-manager.ts index be50909fb..8212dd1f7 100644 --- a/src/lib/detection/package-manager.ts +++ b/src/lib/detection/package-manager.ts @@ -1,90 +1,36 @@ /** * Cross-ecosystem package manager detection. * - * Provides a common interface (PackageManagerDetector) that each FrameworkConfig - * implements, plus shared helpers for Node.js, Python, PHP, and Swift ecosystems. - * The MCP tool in wizard-tools.ts delegates to whatever detector the - * current framework supplies. + * Each FrameworkConfig implements the PackageManagerDetector contract; the + * helpers here cover the Python, PHP, Swift, Ruby, Rust, Elixir, Go, Flutter, + * Android and Java ecosystems (Node is in `@utils/package-manager`). The + * `detect_package_manager` tool delegates to whatever detector the current + * framework supplies. */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { - detectAllPackageManagers, - type PackageManager, + detectNodePackageManagers, + type DetectedPackageManager, + type PackageManagerDetector, + type PackageManagerInfo, } from '@utils/package-manager'; + +// The detector contract and the Node detector live in `@utils/package-manager` +// (the agent's tools need them without importing detection); re-exported so +// every framework keeps its import path. +export { detectNodePackageManagers }; +export type { + DetectedPackageManager, + PackageManagerDetector, + PackageManagerInfo, +}; import { detectPackageManager as detectPythonPM, PythonPackageManager, } from '@frameworks/python/utils'; -// --------------------------------------------------------------------------- -// Common types -// --------------------------------------------------------------------------- - -/** Structured package manager info the agent can act on */ -export interface DetectedPackageManager { - name: string; - label: string; - installCommand: string; - runCommand?: string; -} - -/** Result returned by every detector */ -export interface PackageManagerInfo { - detected: DetectedPackageManager[]; - primary: DetectedPackageManager | null; - recommendation: string; -} - -/** Signature each framework implements */ -export type PackageManagerDetector = ( - installDir: string, -) => Promise; - -// --------------------------------------------------------------------------- -// Node.js helper -// --------------------------------------------------------------------------- - -function serializeNodePM(pm: PackageManager): DetectedPackageManager { - return { - name: pm.name, - label: pm.label, - installCommand: pm.installCommand, - runCommand: pm.runScriptCommand, - }; -} - -/** - * Detect Node.js package managers via lockfiles. - * Wraps the existing detectAllPackageManagers() from utils/package-manager.ts. - */ -export function detectNodePackageManagers( - installDir: string, -): Promise { - const detected = detectAllPackageManagers({ installDir }).map( - serializeNodePM, - ); - - if (detected.length === 0) { - return Promise.resolve({ - detected: [], - primary: null, - recommendation: 'No lockfile found. Default to npm (npm add, npm run).', - }); - } - - const primary = detected[0]; - return Promise.resolve({ - detected, - primary, - recommendation: - detected.length === 1 - ? `Use ${primary.label} (${primary.installCommand}).` - : `Multiple package managers detected. Prefer ${primary.label} (${primary.installCommand}).`, - }); -} - // --------------------------------------------------------------------------- // Python helper // --------------------------------------------------------------------------- diff --git a/src/lib/wizard-tools/mcp.ts b/src/lib/wizard-tools/mcp.ts index 15fafd432..c79510a9b 100644 --- a/src/lib/wizard-tools/mcp.ts +++ b/src/lib/wizard-tools/mcp.ts @@ -14,7 +14,7 @@ import { z } from 'zod'; import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; import { makeMutex } from '@utils/atomic-ledger'; -import type { PackageManagerDetector } from '../detection/package-manager'; +import type { PackageManagerDetector } from '@utils/package-manager'; import { AUDIT_CHECKS_FILE, type AuditCheck, diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 1339d602e..2c3790fda 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -170,3 +170,70 @@ export function detectAllPackageManagers({ return matches; }); } + +// --------------------------------------------------------------------------- +// The detector contract the agent's `detect_package_manager` tool calls +// --------------------------------------------------------------------------- + +/** Structured package manager info the agent can act on */ +export interface DetectedPackageManager { + name: string; + label: string; + installCommand: string; + runCommand?: string; +} + +/** Result returned by every detector */ +export interface PackageManagerInfo { + detected: DetectedPackageManager[]; + primary: DetectedPackageManager | null; + recommendation: string; +} + +/** Signature each framework implements */ +export type PackageManagerDetector = ( + installDir: string, +) => Promise; + +// --------------------------------------------------------------------------- +// Node.js helper +// --------------------------------------------------------------------------- + +function serializeNodePM(pm: PackageManager): DetectedPackageManager { + return { + name: pm.name, + label: pm.label, + installCommand: pm.installCommand, + runCommand: pm.runScriptCommand, + }; +} + +/** + * Detect Node.js package managers via lockfiles. + * Wraps detectAllPackageManagers() above. + */ +export function detectNodePackageManagers( + installDir: string, +): Promise { + const detected = detectAllPackageManagers({ installDir }).map( + serializeNodePM, + ); + + if (detected.length === 0) { + return Promise.resolve({ + detected: [], + primary: null, + recommendation: 'No lockfile found. Default to npm (npm add, npm run).', + }); + } + + const primary = detected[0]; + return Promise.resolve({ + detected, + primary, + recommendation: + detected.length === 1 + ? `Use ${primary.label} (${primary.installCommand}).` + : `Multiple package managers detected. Prefer ${primary.label} (${primary.installCommand}).`, + }); +} From 0b98bd370e9fc3d2e4f4cefa79e18065717648e1 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:29:25 -0400 Subject: [PATCH 16/61] refactor(middleware): report benchmark lines as progress events createBenchmarkPipeline takes the run's emitter; the summary and JSON writer plugins receive it through MiddlewareFactoryOptions and emit one `log` event per line they used to print through getUI(). The legacy adapter's reducer maps each back to WizardUI.log.info, so --benchmark output is unchanged. Nothing under src/lib/middleware imports src/ui. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/agent/runner/sequence/linear.ts | 2 +- .../__tests__/benchmark-emit.test.ts | 104 ++++++++++++++++++ src/lib/middleware/benchmark.ts | 15 +-- src/lib/middleware/benchmarks/index.ts | 4 +- src/lib/middleware/benchmarks/json-writer.ts | 8 +- src/lib/middleware/benchmarks/summary.ts | 24 ++-- src/lib/middleware/types.ts | 4 +- 7 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 src/lib/middleware/__tests__/benchmark-emit.test.ts diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index 8fe2a8df9..e45358fd1 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -75,7 +75,7 @@ export async function runLinearProgram({ }); const middleware = input.flags.benchmark - ? createBenchmarkPipeline(spinner, runOptions(input)) + ? createBenchmarkPipeline(emit, spinner, runOptions(input)) : undefined; // 7. Build prompt diff --git a/src/lib/middleware/__tests__/benchmark-emit.test.ts b/src/lib/middleware/__tests__/benchmark-emit.test.ts new file mode 100644 index 000000000..2b1979e68 --- /dev/null +++ b/src/lib/middleware/__tests__/benchmark-emit.test.ts @@ -0,0 +1,104 @@ +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { AgentProgress } from '@lib/agent/progress'; + +// The benchmark pipeline runs inside the agent, which has no UI. Reaching one +// is the defect this file guards against. +vi.mock('@ui', () => ({ + getUI: () => { + throw new Error('agent code reached the UI'); + }, +})); +vi.mock('@utils/debug', () => ({ + logToFile: vi.fn(), + configureLogFile: vi.fn(), + getLogFilePath: () => '/tmp/wizard.log', +})); + +import { createBenchmarkPipeline } from '../benchmark'; +import { getDefaultConfig } from '../config'; + +describe('createBenchmarkPipeline', () => { + let dir: string; + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'wizard-benchmark-')); + }); + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it('reports every benchmark line as a progress event and never touches the UI', () => { + const events: AgentProgress[] = []; + const spinner = { start: vi.fn(), stop: vi.fn(), message: vi.fn() }; + const config = getDefaultConfig(); + config.output.benchmarkPath = join(dir, 'benchmark.json'); + config.output.logPath = join(dir, 'wizard.log'); + config.output.logEnabled = false; + + const pipeline = createBenchmarkPipeline( + (event) => events.push(event), + spinner, + { + installDir: dir, + ci: false, + debug: false, + benchmark: true, + yaraReport: false, + signup: false, + }, + config, + ); + pipeline.onMessage({ + type: 'assistant', + message: { role: 'assistant', content: [{ type: 'text', text: 'hi' }] }, + }); + pipeline.finalize({ type: 'result', modelUsage: {}, num_turns: 1 }, 1500); + + const logs = events.flatMap((event) => + event.kind === 'log' ? [event.message] : [], + ); + expect(logs[0]).toContain('Verbose logs: /tmp/wizard.log'); + expect(logs[1]).toContain( + `Benchmark data will be written to: ${config.output.benchmarkPath}`, + ); + expect(logs.some((line) => line.includes('Summary by phase'))).toBe(true); + expect(logs.at(-1)).toContain( + `Results written to ${config.output.benchmarkPath}`, + ); + expect(events.every((event) => event.kind === 'log')).toBe(true); + expect(existsSync(config.output.benchmarkPath)).toBe(true); + }); + + it('stays quiet when the config suppresses wizard logs', () => { + const events: AgentProgress[] = []; + const spinner = { start: vi.fn(), stop: vi.fn(), message: vi.fn() }; + const config = getDefaultConfig(); + config.output.benchmarkPath = join(dir, 'benchmark.json'); + config.output.logPath = join(dir, 'wizard.log'); + config.output.logEnabled = false; + config.output.suppressWizardLogs = true; + + const pipeline = createBenchmarkPipeline( + (event) => events.push(event), + spinner, + { + installDir: dir, + ci: false, + debug: false, + benchmark: true, + yaraReport: false, + signup: false, + }, + config, + ); + pipeline.finalize({ type: 'result', modelUsage: {} }, 10); + + const logs = events.flatMap((event) => + event.kind === 'log' ? [event.message] : [], + ); + // Only the JSON writer speaks; the summary plugin is disabled by the flag. + expect(logs).toHaveLength(1); + expect(logs[0]).toContain('Results written to'); + }); +}); diff --git a/src/lib/middleware/benchmark.ts b/src/lib/middleware/benchmark.ts index bc9fc3e7b..08c044253 100644 --- a/src/lib/middleware/benchmark.ts +++ b/src/lib/middleware/benchmark.ts @@ -2,13 +2,12 @@ * Benchmark tracking for wizard runs. * * Usage: - * const pipeline = createBenchmarkPipeline(spinner, options); + * const pipeline = createBenchmarkPipeline(emit, spinner, options); * pipeline.onMessage(message); * pipeline.finalize(resultMessage, durationMs); */ -import { getUI } from '@ui'; -import type { SpinnerHandle } from '@lib/agent/progress'; +import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; import { logToFile, getLogFilePath, configureLogFile } from '@utils/debug'; import { MiddlewarePipeline } from './pipeline'; import { PhaseDetector } from './phase-detector'; @@ -66,11 +65,14 @@ export interface BenchmarkData { * Loads .benchmark-config.json from the install dir, falls back to defaults. */ export function createBenchmarkPipeline( + emit: ProgressEmitter, spinner: SpinnerHandle, options: WizardRunOptions, configOverride?: BenchmarkConfig, ): MiddlewarePipeline { const config = configOverride ?? loadBenchmarkConfig(options.installDir); + const info = (message: string) => + emit({ kind: 'log', level: 'info', message }); configureLogFile({ path: config.output.logPath, @@ -78,16 +80,15 @@ export function createBenchmarkPipeline( }); const plugins = createPluginsFromConfig(config, { + emit, spinner, phased: false, outputPath: config.output.benchmarkPath, }); if (!config.output.suppressWizardLogs) { - getUI().log.info( - `${AgentSignals.BENCHMARK} Verbose logs: ${getLogFilePath()}`, - ); - getUI().log.info( + info(`${AgentSignals.BENCHMARK} Verbose logs: ${getLogFilePath()}`); + info( `${AgentSignals.BENCHMARK} Benchmark data will be written to: ${config.output.benchmarkPath}`, ); } diff --git a/src/lib/middleware/benchmarks/index.ts b/src/lib/middleware/benchmarks/index.ts index 470dbd82b..33491a8db 100644 --- a/src/lib/middleware/benchmarks/index.ts +++ b/src/lib/middleware/benchmarks/index.ts @@ -30,8 +30,8 @@ const PLUGIN_REGISTRY: Record = { contextSize: () => new ContextSizeTrackerPlugin(), cost: () => new CostTrackerPlugin(), duration: () => new DurationTrackerPlugin(), - summary: (opts) => new SummaryPlugin(opts.spinner!), - jsonWriter: (opts) => new JsonWriterPlugin(opts.outputPath!), + summary: (opts) => new SummaryPlugin(opts.spinner!, opts.emit), + jsonWriter: (opts) => new JsonWriterPlugin(opts.outputPath!, opts.emit), }; /** diff --git a/src/lib/middleware/benchmarks/json-writer.ts b/src/lib/middleware/benchmarks/json-writer.ts index 253225335..a68cad9c6 100644 --- a/src/lib/middleware/benchmarks/json-writer.ts +++ b/src/lib/middleware/benchmarks/json-writer.ts @@ -6,7 +6,7 @@ */ import fs from 'fs'; -import { getUI } from '@ui'; +import type { ProgressEmitter } from '@lib/agent/progress'; import { logToFile } from '@utils/debug'; import { AgentSignals } from '@lib/agent/agent-interface'; import type { @@ -56,9 +56,11 @@ export class JsonWriterPlugin implements Middleware { readonly name = 'jsonWriter'; private outputPath: string; + private readonly info: (message: string) => void; - constructor(outputPath: string) { + constructor(outputPath: string, emit: ProgressEmitter) { this.outputPath = outputPath; + this.info = (message) => emit({ kind: 'log', level: 'info', message }); } onFinalize( @@ -170,7 +172,7 @@ export class JsonWriterPlugin implements Middleware { try { fs.writeFileSync(this.outputPath, JSON.stringify(data, null, 2)); logToFile(`Benchmark data written to ${this.outputPath}`); - getUI().log.info( + this.info( `● ${AgentSignals.BENCHMARK} Results written to ${this.outputPath}`, ); } catch (error) { diff --git a/src/lib/middleware/benchmarks/summary.ts b/src/lib/middleware/benchmarks/summary.ts index 1900f9cc5..be1369561 100644 --- a/src/lib/middleware/benchmarks/summary.ts +++ b/src/lib/middleware/benchmarks/summary.ts @@ -1,5 +1,4 @@ -import { getUI } from '@ui'; -import type { SpinnerHandle } from '@lib/agent/progress'; +import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; import { AgentSignals } from '@lib/agent/agent-interface'; import type { Middleware, @@ -98,8 +97,11 @@ export class SummaryPlugin implements Middleware { private spinner: SpinnerHandle; - constructor(spinner: SpinnerHandle) { + private readonly info: (message: string) => void; + + constructor(spinner: SpinnerHandle, emit: ProgressEmitter) { this.spinner = spinner; + this.info = (message) => emit({ kind: 'log', level: 'info', message }); } onPhaseTransition( @@ -118,7 +120,7 @@ export class SummaryPlugin implements Middleware { this.spinner.stop(`${AgentSignals.BENCHMARK} ${fromPhase}`); } - getUI().log.info(`${AgentSignals.BENCHMARK} Starting phase: ${toPhase}`); + this.info(`${AgentSignals.BENCHMARK} Starting phase: ${toPhase}`); this.spinner.start(`Integrating PostHog (${toPhase})...`); } @@ -136,31 +138,31 @@ export class SummaryPlugin implements Middleware { const phaseCount = duration?.phaseSnapshots.length ?? 0; const totalCost = cost?.totalCost ?? 0; - getUI().log.info(''); - getUI().log.info( + this.info(''); + this.info( `◇ ${AgentSignals.BENCHMARK} ${phaseCount} phases in ${fmtDuration( totalDurationMs, )}, cost: ${fmtCost(totalCost)}`, ); - getUI().log.info( + this.info( ` total in: ${fmtTok(tokens?.totalInput ?? 0)}, out: ${fmtTok( tokens?.totalOutput ?? 0, )}, cache_read: ${fmtTok(cache?.totalRead ?? 0)}, cache_5m: ${fmtTok( cache?.totalCreation5m ?? 0, )}, cache_1h: ${fmtTok(cache?.totalCreation1h ?? 0)}`, ); - getUI().log.info(''); - getUI().log.info(`● ${AgentSignals.BENCHMARK} Summary by phase:`); + this.info(''); + this.info(`● ${AgentSignals.BENCHMARK} Summary by phase:`); if (duration?.phaseSnapshots) { for (let i = 0; i < duration.phaseSnapshots.length; i++) { const stats = getPhaseStats(i, ctx); if (stats) { - getUI().log.info(printPhase(stats)); + this.info(printPhase(stats)); } } } - getUI().log.info(''); + this.info(''); } } diff --git a/src/lib/middleware/types.ts b/src/lib/middleware/types.ts index 9e84580e0..8ccd15ec9 100644 --- a/src/lib/middleware/types.ts +++ b/src/lib/middleware/types.ts @@ -5,7 +5,7 @@ * and can publish data to a shared store for downstream middleware to read. */ -import type { SpinnerHandle } from '@lib/agent/progress'; +import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; export type SDKMessage = any; @@ -54,6 +54,8 @@ export interface Middleware { /** Options bag passed to middleware factories during construction */ export interface MiddlewareFactoryOptions { + /** Where a plugin's user-facing lines go: the run's progress events. */ + emit: ProgressEmitter; spinner?: SpinnerHandle; outputPath?: string; phased?: boolean; From 856b613da0a56fff5fa5fee478f9370e481c31fd Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:30:22 -0400 Subject: [PATCH 17/61] refactor(yara): return the scan report line instead of printing it flushScanReport takes `{ yaraReport }` and returns the report line when it wrote one. The runner emits it as a `log` progress event from its single flush seam; the legacy adapter's cleanup prints it through the UI as before. yara-hooks no longer imports the UI or the session type. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/__tests__/yara-flush-report.test.ts | 58 +++++++++++++++++++ .../__tests__/run-agent-standalone.test.ts | 1 + src/lib/agent/runner/index.ts | 3 +- src/lib/programs/run-agent-legacy.ts | 5 +- src/lib/yara-hooks.ts | 17 +++--- 5 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/lib/__tests__/yara-flush-report.test.ts diff --git a/src/lib/__tests__/yara-flush-report.test.ts b/src/lib/__tests__/yara-flush-report.test.ts new file mode 100644 index 000000000..06e46e5c3 --- /dev/null +++ b/src/lib/__tests__/yara-flush-report.test.ts @@ -0,0 +1,58 @@ +import fs from 'fs'; +import { WIZARD_YARA_REPORT_FILE } from '@utils/paths'; + +// The flush runs inside the agent, which has no UI: the report line is +// returned to the caller, who decides where it goes. +vi.mock('@ui', () => ({ + getUI: () => { + throw new Error('agent code reached the UI'); + }, +})); +vi.mock('@utils/debug'); +vi.mock('@utils/analytics', () => ({ + analytics: { wizardCapture: vi.fn(), captureException: vi.fn() }, +})); +vi.mock('fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { ...actual, writeFileSync: vi.fn() }, + writeFileSync: vi.fn(), + }; +}); + +import { + flushScanReport, + recordExternalScan, + resetScanReport, +} from '@lib/yara-hooks'; + +describe('flushScanReport', () => { + beforeEach(() => { + resetScanReport(); + vi.mocked(fs.writeFileSync).mockClear(); + }); + + it('returns the report line once when a report was requested', () => { + recordExternalScan('PostToolUse', 'Write', [], 'warned'); + + const line = flushScanReport({ yaraReport: true }); + + expect(line).toContain(`YARA scan report: ${WIZARD_YARA_REPORT_FILE}`); + expect(line).toContain('1 tool calls scanned, 0 violations detected'); + expect(fs.writeFileSync).toHaveBeenCalledWith( + WIZARD_YARA_REPORT_FILE, + expect.stringContaining('"totalScans": 1'), + ); + // Idempotent: the first flush zeroed the scan state. + expect(flushScanReport({ yaraReport: true })).toBeUndefined(); + }); + + it('returns nothing without --yara-report, but still flushes the state', () => { + recordExternalScan('PostToolUse', 'Write', [], 'warned'); + + expect(flushScanReport({ yaraReport: false })).toBeUndefined(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(flushScanReport({ yaraReport: true })).toBeUndefined(); + }); +}); diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts index 481e8a5eb..c419a2e48 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -443,6 +443,7 @@ describe('runAgent standalone', () => { }); vi.mocked(flushScanReport).mockImplementationOnce(() => { order.push('scan-flush'); + return undefined; }); const result = await runAgent( config({ diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index 70f213775..979a19723 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -123,6 +123,7 @@ export async function runAgent( snapshot: collector.snapshot(), }; } finally { - flushScanReport({ yaraReport: input.flags.yaraReport }); + const report = flushScanReport({ yaraReport: input.flags.yaraReport }); + if (report) log(report); } } diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index 87b8f8e36..f26bc571b 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -198,7 +198,10 @@ async function runProgram( // Cleanup coverage for the abort/cancel path: `wizardAbort` runs the // registered cleanups, and the agent's own `finally` covers completion. // flushScanReport is idempotent, so the overlap is a harmless no-op. - registerCleanup(() => flushScanReport({ yaraReport: session.yaraReport })); + registerCleanup(() => { + const report = flushScanReport({ yaraReport: session.yaraReport }); + if (report) ui.log.info(report); + }); // Linear settings restoration fires on entry to the outro screen, so it // is registered before the run can reach that screen. Same owner, same diff --git a/src/lib/yara-hooks.ts b/src/lib/yara-hooks.ts index cd3d052f0..6f97de2ac 100644 --- a/src/lib/yara-hooks.ts +++ b/src/lib/yara-hooks.ts @@ -35,8 +35,6 @@ import type { import { logToFile } from '@utils/debug'; import { readFileHead } from '@utils/bounded-fs'; import { analytics } from '@utils/analytics'; -import { getUI } from '@ui'; -import type { WizardSession } from '@lib/wizard-session'; import { isSkillInstallCommand } from './skill-install'; import { highestSeverityMatch, @@ -425,18 +423,23 @@ export function captureScanReport(): void { * which is what makes this whole function idempotent — a second call from * another termination path (e.g. finally after an abort already flushed) finds * scanCount === 0 and every step no-ops. + * + * Returns the user-facing report line when a report was written; the caller + * decides where it goes (the runner emits it as progress). */ -export function flushScanReport( - session: Pick, -): void { - if (session.yaraReport) { +export function flushScanReport(options: { + yaraReport: boolean; +}): string | undefined { + let line: string | undefined; + if (options.yaraReport) { const reportPath = writeScanReport(); if (reportPath) { const summary = formatScanReport(); - getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ''}`); + line = `YARA scan report: ${reportPath}${summary ?? ''}`; } } captureScanReport(); + return line; } // ─── Wizard-documentation allowlist ─────────────────────────────── From 13fa0a2882e197f7448ed82534a86ee392c4c024 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:33:12 -0400 Subject: [PATCH 18/61] refactor(agent): publish the handoff as a progress event publish_handoff emits `{ kind: 'handoff', text }` instead of calling getUI().setHandoffText. Both facades (the MCP server and the pi tools) receive the run's emitter, the snapshot keeps the text as RunSnapshot.handoffText, and the legacy adapter's reducer maps the event to WizardUI.setHandoffText so the TUI and the headless host see exactly what they saw before. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../architecture/known-violations.json | 1 + .../__tests__/progress-collector.test.ts | 8 ++++ src/lib/agent/agent-interface.ts | 1 + src/lib/agent/progress.ts | 2 + src/lib/agent/runner/harness/pi/index.ts | 1 + src/lib/agent/runner/harness/pi/task.ts | 3 +- src/lib/agent/runner/harness/pi/tools.ts | 7 ++- .../agent/runner/shared/progress-collector.ts | 3 ++ src/lib/agent/runner/shared/types.ts | 2 + .../wizard-tools/__tests__/handoff.test.ts | 47 ++++++++++--------- src/lib/wizard-tools/handoff.ts | 10 ++-- src/lib/wizard-tools/mcp.ts | 7 ++- src/ui/__tests__/agent-progress.test.ts | 3 ++ src/ui/agent-progress.ts | 3 ++ 14 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index d270a05bb..cac070705 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -49,6 +49,7 @@ "src/lib/wizard-ask-bridge.ts -> src/lib/agent/progress.ts", "src/lib/wizard-session.ts -> src/lib/agent/claude-settings.ts", "src/lib/wizard-session.ts -> src/lib/agent/progress.ts", + "src/lib/wizard-tools/handoff.ts -> src/lib/agent/progress.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", diff --git a/src/lib/agent/__tests__/progress-collector.test.ts b/src/lib/agent/__tests__/progress-collector.test.ts index 061292550..1d6e0740f 100644 --- a/src/lib/agent/__tests__/progress-collector.test.ts +++ b/src/lib/agent/__tests__/progress-collector.test.ts @@ -37,3 +37,11 @@ it('isolates nested completion data from a mutating observer', () => { nextSteps: { heading: 'Next', items: ['Keep the report'] }, }); }); + +it('keeps the published handoff text in the snapshot', () => { + const collector = createProgressCollector(); + expect(collector.snapshot().handoffText).toBeUndefined(); + collector.emit({ kind: 'handoff', text: '# Report' }); + collector.emit({ kind: 'handoff', text: '# Report, revised' }); + expect(collector.snapshot().handoffText).toBe('# Report, revised'); +}); diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index d4e9a3bcd..2b47993ca 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -644,6 +644,7 @@ export async function initializeAgent( askMaxQuestions: config.askMaxQuestions, orchestrator: config.orchestrator, triageProvider, + emit: config.emit, }); mcpServers['wizard-tools'] = wizardToolsServer; diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index eb5386ff4..3bc45c79e 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -229,6 +229,8 @@ export type AgentProgress = | { kind: 'finalCost'; usd: number } /** The gateway returned 401; a failure follows (`WizardUI.showAuthError`). */ | { kind: 'authError'; detail: AuthErrorDetail } + /** The handoff document the agent published (`WizardUI.setHandoffText`). */ + | { kind: 'handoff'; text: string } /** The run's final outro payload (`WizardUI.setOutroData`). */ | { kind: 'completion'; outro: OutroData }; diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index 82c705615..b14df2a37 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -434,6 +434,7 @@ export const piBackend: AgentHarness = { workingDirectory: input.installDir, skillsBaseUrl: boot.skillsBaseUrl, triageProvider: boot.triageProvider, + emit, detectPackageManager: config.detectPackageManager, // The host ask bridge — lets interactive programs (self-driving) ask // the user through pi. Threaded from the runner, same path as the diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 9d7adc97c..af9017a21 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -361,13 +361,14 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { // Wizard env + package-manager tools are always on — their handlers are // fenced, and init/build tasks depend on them. publish_handoff rides - // along (store-only handler) so the report task can publish the handoff. + // along (it only emits) so the report task can publish the handoff. const { createWizardPiTools } = await import('./tools'); const wizardToolNames = allowedPiWizardTools(allowedTools); const wizardTools = createWizardPiTools({ workingDirectory: dir, skillsBaseUrl: boot.skillsBaseUrl, triageProvider: boot.triageProvider, + emit, // Present only for a task allowed to ask; without it wizard_ask errors // instead of hanging on a prompt nobody will ever see. askBridge, diff --git a/src/lib/agent/runner/harness/pi/tools.ts b/src/lib/agent/runner/harness/pi/tools.ts index 360cf0df1..117d7c349 100644 --- a/src/lib/agent/runner/harness/pi/tools.ts +++ b/src/lib/agent/runner/harness/pi/tools.ts @@ -54,6 +54,7 @@ import { WIZARD_ASK_TOOL_DESCRIPTION, } from '@lib/wizard-tools/tools'; import type { LLMProvider } from '@posthog/warlock'; +import type { ProgressEmitter } from '@lib/agent/progress'; import { isFullyCancelled, type WizardAskBridge } from '@lib/wizard-ask-bridge'; import { PUBLISH_HANDOFF_CONTENT_DESCRIPTION, @@ -96,6 +97,8 @@ export interface PiToolsContext { disallowedTools?: readonly string[]; /** Scan-triage classifier, resolved once in bootstrap. Absent → scans fail closed. */ triageProvider?: LLMProvider; + /** Where `publish_handoff` reports. Absent → the handoff is written but reported nowhere. */ + emit?: ProgressEmitter; } export function createWizardPiTools(ctx: PiToolsContext): ToolDefinition[] { @@ -556,7 +559,7 @@ export function createWizardPiTools(ctx: PiToolsContext): ToolDefinition[] { }), }), execute(_id, args) { - const result = publishHandoff(args.content); + const result = publishHandoff(args.content, ctx.emit); logToFile(`[pi] publish_handoff: ${result.message}`); return Promise.resolve(text(result.message)); }, @@ -572,7 +575,7 @@ export function createWizardPiTools(ctx: PiToolsContext): ToolDefinition[] { withMode(auditSeedChecks, 'parallel'), withMode(auditAddChecks, 'parallel'), withMode(auditResolveChecks, 'parallel'), - // Sequential: it mutates the store's handoff state. + // Sequential: it publishes the run's handoff. withMode(publishHandoffTool, 'sequential'), ]; // Register wizard_ask only when the program allows it. posthog-integration diff --git a/src/lib/agent/runner/shared/progress-collector.ts b/src/lib/agent/runner/shared/progress-collector.ts index 6f493dbc3..42a9da5f8 100644 --- a/src/lib/agent/runner/shared/progress-collector.ts +++ b/src/lib/agent/runner/shared/progress-collector.ts @@ -63,6 +63,9 @@ export function createProgressCollector( case 'finalCost': snapshot.finalCostUsd = event.usd; break; + case 'handoff': + snapshot.handoffText = event.text; + break; default: break; } diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index db73f8429..ea339af3e 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -279,6 +279,8 @@ export interface RunSnapshot { finalCostUsd?: number; dashboardUrl?: string; notebookUrl?: string; + /** The handoff document the agent published, when it did. */ + handoffText?: string; } /** A sequence decides an outcome; the dispatcher owns its snapshot. */ diff --git a/src/lib/wizard-tools/__tests__/handoff.test.ts b/src/lib/wizard-tools/__tests__/handoff.test.ts index bbe2aae14..c93b792c8 100644 --- a/src/lib/wizard-tools/__tests__/handoff.test.ts +++ b/src/lib/wizard-tools/__tests__/handoff.test.ts @@ -11,34 +11,35 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { getUI, setUI } from '@ui'; -import type { WizardUI } from '@ui/wizard-ui'; +import type { AgentProgress } from '@lib/agent/progress'; import { MAX_HANDOFF_TEXT_CHARS, publishHandoff } from '../handoff'; +// The tool runs inside the agent, which has no UI; the handoff is a progress +// event the host projects however it likes. +vi.mock('@ui', () => ({ + getUI: () => { + throw new Error('agent code reached the UI'); + }, +})); + describe('publishHandoff', () => { const captured: string[] = []; - let previousUI: WizardUI; + const emit = (event: AgentProgress) => { + if (event.kind === 'handoff') captured.push(event.text); + }; let temporaryDirectory: string; let ambientOutputPath: string | undefined; beforeEach(() => { captured.length = 0; - previousUI = getUI(); temporaryDirectory = mkdtempSync(join(tmpdir(), 'wizard-handoff-')); // Save the ambient value so a developer running these tests with the var // exported doesn't have their environment silently clobbered. ambientOutputPath = process.env.POSTHOG_HANDOFF_OUTPUT_PATH; delete process.env.POSTHOG_HANDOFF_OUTPUT_PATH; - setUI({ - ...previousUI, - setHandoffText: (text: string) => { - captured.push(text); - }, - } as WizardUI); }); afterEach(() => { - setUI(previousUI); rmSync(temporaryDirectory, { recursive: true, force: true }); if (ambientOutputPath === undefined) { delete process.env.POSTHOG_HANDOFF_OUTPUT_PATH; @@ -47,15 +48,15 @@ describe('publishHandoff', () => { } }); - it('publishes the content through the UI seam', () => { - const result = publishHandoff('# Setup report\n\nAll done.'); + it('publishes the content as a handoff progress event', () => { + const result = publishHandoff('# Setup report\n\nAll done.', emit); expect(result.ok).toBe(true); expect(captured).toEqual(['# Setup report\n\nAll done.']); }); it('rejects blank content instead of publishing', () => { for (const bad of ['', ' \n']) { - const result = publishHandoff(bad); + const result = publishHandoff(bad, emit); expect(result.ok).toBe(false); expect(result.message).toContain('complete report markdown'); } @@ -66,7 +67,7 @@ describe('publishHandoff', () => { const outputPath = join(temporaryDirectory, 'handoff.md'); process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; const oversized = 'x'.repeat(MAX_HANDOFF_TEXT_CHARS + 10); - const result = publishHandoff(oversized); + const result = publishHandoff(oversized, emit); expect(result.ok).toBe(true); expect(captured[0]).toHaveLength(MAX_HANDOFF_TEXT_CHARS); @@ -80,7 +81,7 @@ describe('publishHandoff', () => { const outputPath = join(temporaryDirectory, 'handoff.md'); process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; - const result = publishHandoff('# Setup report\n\nAll done.'); + const result = publishHandoff('# Setup report\n\nAll done.', emit); expect(result.ok).toBe(true); expect(readFileSync(outputPath, 'utf8')).toBe( @@ -91,7 +92,7 @@ describe('publishHandoff', () => { it('continues publishing when the host-provided output path cannot be written', () => { process.env.POSTHOG_HANDOFF_OUTPUT_PATH = temporaryDirectory; - const result = publishHandoff('# Setup report\n\nAll done.'); + const result = publishHandoff('# Setup report\n\nAll done.', emit); expect(result.ok).toBe(true); expect(captured).toEqual(['# Setup report\n\nAll done.']); @@ -103,7 +104,7 @@ describe('publishHandoff', () => { const outputPath = join(temporaryDirectory, 'handoff.md'); process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; - publishHandoff('# Setup report\n\nAll done.'); + publishHandoff('# Setup report\n\nAll done.', emit); expect(statSync(outputPath).mode & 0o777).toBe(0o600); }); @@ -114,7 +115,7 @@ describe('publishHandoff', () => { chmodSync(outputPath, 0o644); // umask may already restrict; be explicit process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; - publishHandoff('# Setup report\n\nAll done.'); + publishHandoff('# Setup report\n\nAll done.', emit); expect(readFileSync(outputPath, 'utf8')).toBe( '# Setup report\n\nAll done.', @@ -129,7 +130,7 @@ describe('publishHandoff', () => { symlinkSync(victim, outputPath); process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; - const result = publishHandoff('# Malicious report'); + const result = publishHandoff('# Malicious report', emit); // The user-facing handoff still succeeds (fail-open)… expect(result.ok).toBe(true); @@ -148,7 +149,7 @@ describe('publishHandoff', () => { it('refuses a relative output path and stays fail-open', () => { process.env.POSTHOG_HANDOFF_OUTPUT_PATH = 'handoff.md'; - const result = publishHandoff('# Setup report\n\nAll done.'); + const result = publishHandoff('# Setup report\n\nAll done.', emit); expect(result.ok).toBe(true); expect(captured).toEqual(['# Setup report\n\nAll done.']); @@ -159,7 +160,7 @@ describe('publishHandoff', () => { const outputPath = join(temporaryDirectory, 'handoff.md'); process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; - publishHandoff('# Setup report\n\nAll done.'); + publishHandoff('# Setup report\n\nAll done.', emit); expect(readdirSync(temporaryDirectory)).toEqual(['handoff.md']); }); @@ -169,7 +170,7 @@ describe('publishHandoff', () => { writeFileSync(outputPath, '# Previous run\n\nStale content.', 'utf8'); process.env.POSTHOG_HANDOFF_OUTPUT_PATH = outputPath; - publishHandoff('# Current run\n\nFresh content.'); + publishHandoff('# Current run\n\nFresh content.', emit); expect(readFileSync(outputPath, 'utf8')).toBe( '# Current run\n\nFresh content.', diff --git a/src/lib/wizard-tools/handoff.ts b/src/lib/wizard-tools/handoff.ts index 4dedf0428..67411da68 100644 --- a/src/lib/wizard-tools/handoff.ts +++ b/src/lib/wizard-tools/handoff.ts @@ -1,9 +1,10 @@ /** * publish_handoff — the agent publishes the run's handoff doc (the report * markdown) in one explicit call, replacing the report file + watcher path. + * It leaves the tool as a `handoff` progress event; the host projects it. */ -import { getUI } from '@ui'; +import type { ProgressEmitter } from '@lib/agent/progress'; import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; import { runtimeEnv } from '@env'; @@ -134,7 +135,10 @@ function writeHandoffFileAtomically( } } -export function publishHandoff(content: string): PublishHandoffResult { +export function publishHandoff( + content: string, + emit: ProgressEmitter | undefined, +): PublishHandoffResult { if (content.trim() === '') { analytics.wizardCapture('handoff published', { handoff_ok: false, @@ -148,7 +152,7 @@ export function publishHandoff(content: string): PublishHandoffResult { } const truncated = content.length > MAX_HANDOFF_TEXT_CHARS; const text = truncated ? content.slice(0, MAX_HANDOFF_TEXT_CHARS) : content; - getUI().setHandoffText(text); + emit?.({ kind: 'handoff', text }); const handoffOutputPath = runtimeEnv('POSTHOG_HANDOFF_OUTPUT_PATH'); let handoffOutputWritten: boolean | undefined; diff --git a/src/lib/wizard-tools/mcp.ts b/src/lib/wizard-tools/mcp.ts index c79510a9b..1b48eee2a 100644 --- a/src/lib/wizard-tools/mcp.ts +++ b/src/lib/wizard-tools/mcp.ts @@ -28,6 +28,7 @@ import { publishHandoff, } from './handoff'; import { createSecretVault, type SecretVault } from '../secret-vault'; +import type { ProgressEmitter } from '@lib/agent/progress'; import { buildOrchestratorTools, type OrchestratorToolsContext, @@ -147,6 +148,9 @@ export interface WizardToolsOptions { /** Scan-triage classifier for install_skill's scan, resolved by the caller. */ triageProvider: LLMProvider; + + /** Where `publish_handoff` reports. Absent → the handoff is written but reported nowhere. */ + emit?: ProgressEmitter; } /** Default per-run cap on wizard_ask calls when no override is provided. */ @@ -168,6 +172,7 @@ export async function createWizardToolsServer(options: WizardToolsOptions) { secretVault = createSecretVault(), orchestrator, triageProvider, + emit, } = options; const sdk = await getSDKModule(); const { tool, createSdkMcpServer } = sdk; @@ -755,7 +760,7 @@ export async function createWizardToolsServer(options: WizardToolsOptions) { content: z.string().describe(PUBLISH_HANDOFF_CONTENT_DESCRIPTION), }, (args: { content: string }) => { - const result = publishHandoff(args.content); + const result = publishHandoff(args.content, emit); logToFile(`publish_handoff: ${result.message}`); return { content: [{ type: 'text' as const, text: result.message }], diff --git a/src/ui/__tests__/agent-progress.test.ts b/src/ui/__tests__/agent-progress.test.ts index b6bf8c49e..afc4cb788 100644 --- a/src/ui/__tests__/agent-progress.test.ts +++ b/src/ui/__tests__/agent-progress.test.ts @@ -20,6 +20,7 @@ it('projects every progress event onto the matching UI call, in order', () => { 'addTokenUsage', 'setFinalTokenCostUsd', 'showAuthError', + 'setHandoffText', 'setOutroData', ] as const; for (const method of methods) { @@ -72,6 +73,7 @@ it('projects every progress event onto the matching UI call, in order', () => { { kind: 'usage', delta }, { kind: 'finalCost', usd: 1.25 }, { kind: 'authError', detail }, + { kind: 'handoff', text: '# Report' }, { kind: 'completion', outro }, { kind: 'lifecycle', phase: 'completed', message: 'Finished' }, ]; @@ -97,6 +99,7 @@ it('projects every progress event onto the matching UI call, in order', () => { ['addTokenUsage', delta], ['setFinalTokenCostUsd', 1.25], ['showAuthError', detail], + ['setHandoffText', '# Report'], ['setOutroData', outro], ['outro', 'Finished'], ]); diff --git a/src/ui/agent-progress.ts b/src/ui/agent-progress.ts index d3e3450cc..c5c406f65 100644 --- a/src/ui/agent-progress.ts +++ b/src/ui/agent-progress.ts @@ -47,6 +47,9 @@ export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { case 'authError': ui.showAuthError(event.detail); break; + case 'handoff': + ui.setHandoffText(event.text); + break; case 'completion': ui.setOutroData(event.outro); break; From 5cffae63a76335b62bdcec6cd169cde7e5141a28 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Mon, 21 Sep 2026 21:34:21 -0400 Subject: [PATCH 19/61] build(lint): fence the agent's imports ESLint no-restricted-imports on today's agent paths: no @ui, session, detection, registry, runners, commands, steps, frameworks, setup-utils or oauth imports of any kind, no wizardAbort, and @lib/programs as types only until B1 moves PROGRAM_BINDINGS. No file allowlist; the rule runs in the editor and in `pnpm lint`. A2b collapses the path list to src/agent/**. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .eslintrc.cjs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 184dc942a..e3fa6cb1d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -30,6 +30,74 @@ module.exports = { 'prettier', ], overrides: [ + { + // The agent surface. It takes resolved data in, reports through + // progress events and asks through an injected answerer, so nothing + // here may reach a UI, the session, detection, the CLI or a program at + // runtime. Program types stay importable until B1 moves + // PROGRAM_BINDINGS to programs. Today's paths; A2b collapses them to + // src/agent/**. + files: [ + 'src/lib/agent/**/*.ts', + 'src/lib/middleware/**/*.ts', + 'src/lib/wizard-tools/**/*.ts', + 'src/lib/gateway-session.ts', + 'src/lib/safe-tools.ts', + 'src/lib/wizard-ask-bridge.ts', + 'src/lib/yara-hooks.ts', + 'src/lib/yara-policy.ts', + ], + excludedFiles: ['**/__tests__/**'], + rules: { + '@typescript-eslint/no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@utils/wizard-abort', + importNames: ['wizardAbort'], + message: + 'The agent never exits the process: return a failure in RunResult.', + }, + ], + patterns: [ + { + group: [ + '@ui', + '@ui/**', + '**/ui/**', + '@lib/wizard-session', + '**/wizard-session', + '@lib/detection/**', + '**/detection/**', + '@lib/registry', + '**/lib/registry', + '@lib/runners/**', + '**/runners/**', + '**/commands/**', + '@steps', + '@steps/**', + '@frameworks/**', + '**/frameworks/**', + '@utils/setup-utils', + '**/setup-utils', + '@utils/oauth', + '**/utils/oauth', + ], + message: + 'The agent reports through progress events and asks through AgentInteraction; it takes everything else through RunConfig and RunInput.', + }, + { + group: ['@lib/programs/**', '**/programs/**'], + allowTypeImports: true, + message: + 'The agent takes program data through RunConfig. Types only, until B1 moves PROGRAM_BINDINGS to programs.', + }, + ], + }, + ], + }, + }, { files: [ '*.test.js', From 2b8c672da0ad02affe524592f46ccf68a36d6db8 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 00:13:45 -0400 Subject: [PATCH 20/61] refactor(shared): move utilities into shared Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- e2e-tests/mocks/handlers.ts | 2 +- .../architecture/known-violations.json | 6 +++--- src/__tests__/cli.test.ts | 20 +++++++++---------- src/__tests__/provision-cli.test.ts | 14 ++++++------- src/__tests__/wizard-abort.test.ts | 2 +- src/lib/__tests__/agent-interface.test.ts | 4 ++-- .../__tests__/cloudflare-detection.test.ts | 2 +- src/lib/__tests__/wizard-ask-bridge.test.ts | 2 +- src/lib/__tests__/wizard-can-use-tool.test.ts | 4 ++-- src/lib/__tests__/yara-hooks.test.ts | 6 +++--- src/lib/agent/runner/sequence/linear.ts | 4 ++-- .../__tests__/package-manager.test.ts | 4 ++-- .../utils/__fixtures__/provisioning.ts | 0 .../utils/__tests__/analytics.test.ts | 0 .../utils/__tests__/bounded-fs-walk.test.ts | 0 .../utils/__tests__/bounded-fs.test.ts | 0 .../utils/__tests__/ci-flag-overrides.test.ts | 0 .../utils/__tests__/ci-region.test.ts | 0 .../utils/__tests__/debug.test.ts | 0 .../utils/__tests__/env-api-key.test.ts | 0 .../utils/__tests__/env-scan.test.ts | 0 .../utils/__tests__/environment.test.ts | 0 .../utils/__tests__/links.test.ts | 0 .../utils/__tests__/oauth-errors.test.ts | 0 .../utils/__tests__/oauth-refresh.test.ts | 0 .../utils/__tests__/oauth-server.test.ts | 0 .../utils/__tests__/oauth.test.ts | 0 .../__tests__/project-resolution.test.ts | 0 .../utils/__tests__/provisioning.test.ts | 0 .../utils/__tests__/semver.test.ts | 0 .../utils/__tests__/terminal-bell.test.ts | 0 src/{ => shared}/utils/analytics.ts | 0 src/{ => shared}/utils/atomic-ledger.ts | 0 src/{ => shared}/utils/bounded-fs.ts | 0 src/{ => shared}/utils/ci-flag-overrides.ts | 0 src/{ => shared}/utils/clipboard.ts | 0 src/{ => shared}/utils/custom-headers.ts | 0 src/{ => shared}/utils/debug.ts | 0 src/{ => shared}/utils/env-api-key.ts | 0 src/{ => shared}/utils/env-scan.ts | 0 src/{ => shared}/utils/environment.ts | 0 src/{ => shared}/utils/links.ts | 0 src/{ => shared}/utils/oauth-errors.ts | 0 src/{ => shared}/utils/oauth.ts | 0 src/{ => shared}/utils/package-json.ts | 0 src/{ => shared}/utils/package-manager.ts | 2 +- src/{ => shared}/utils/paths.ts | 0 src/{ => shared}/utils/project-resolution.ts | 0 src/{ => shared}/utils/provisioning.ts | 0 src/{ => shared}/utils/rules/astro-rules.md | 0 src/{ => shared}/utils/rules/next-rules.md | 0 .../utils/rules/react-native-rules.md | 0 src/{ => shared}/utils/rules/react-rules.md | 0 src/{ => shared}/utils/rules/svelte-rules.md | 0 src/{ => shared}/utils/rules/universal.md | 0 src/{ => shared}/utils/semver.ts | 0 src/{ => shared}/utils/setup-utils.ts | 2 +- src/{ => shared}/utils/terminal-bell.ts | 0 src/{ => shared}/utils/types.ts | 0 src/{ => shared}/utils/urls.ts | 0 .../utils/vendor/is-unicorn-supported.ts | 0 src/{ => shared}/utils/wizard-abort.ts | 0 .../clients/__tests__/claude-code.test.ts | 4 ++-- .../clients/__tests__/codex.test.ts | 2 +- .../__tests__/install-cli-steering.test.ts | 2 +- src/ui/tui/__tests__/WizardAskScreen.test.ts | 2 +- src/ui/tui/__tests__/exit-line.test.ts | 2 +- src/ui/tui/__tests__/mcp-installer.test.ts | 4 ++-- src/ui/tui/__tests__/store-invariants.test.ts | 2 +- src/ui/tui/__tests__/store.test.ts | 2 +- src/ui/tui/__tests__/task-notice.test.ts | 2 +- tsconfig.build.json | 2 +- vitest.config.ts | 2 +- 73 files changed, 50 insertions(+), 50 deletions(-) rename src/{ => shared}/utils/__fixtures__/provisioning.ts (100%) rename src/{ => shared}/utils/__tests__/analytics.test.ts (100%) rename src/{ => shared}/utils/__tests__/bounded-fs-walk.test.ts (100%) rename src/{ => shared}/utils/__tests__/bounded-fs.test.ts (100%) rename src/{ => shared}/utils/__tests__/ci-flag-overrides.test.ts (100%) rename src/{ => shared}/utils/__tests__/ci-region.test.ts (100%) rename src/{ => shared}/utils/__tests__/debug.test.ts (100%) rename src/{ => shared}/utils/__tests__/env-api-key.test.ts (100%) rename src/{ => shared}/utils/__tests__/env-scan.test.ts (100%) rename src/{ => shared}/utils/__tests__/environment.test.ts (100%) rename src/{ => shared}/utils/__tests__/links.test.ts (100%) rename src/{ => shared}/utils/__tests__/oauth-errors.test.ts (100%) rename src/{ => shared}/utils/__tests__/oauth-refresh.test.ts (100%) rename src/{ => shared}/utils/__tests__/oauth-server.test.ts (100%) rename src/{ => shared}/utils/__tests__/oauth.test.ts (100%) rename src/{ => shared}/utils/__tests__/project-resolution.test.ts (100%) rename src/{ => shared}/utils/__tests__/provisioning.test.ts (100%) rename src/{ => shared}/utils/__tests__/semver.test.ts (100%) rename src/{ => shared}/utils/__tests__/terminal-bell.test.ts (100%) rename src/{ => shared}/utils/analytics.ts (100%) rename src/{ => shared}/utils/atomic-ledger.ts (100%) rename src/{ => shared}/utils/bounded-fs.ts (100%) rename src/{ => shared}/utils/ci-flag-overrides.ts (100%) rename src/{ => shared}/utils/clipboard.ts (100%) rename src/{ => shared}/utils/custom-headers.ts (100%) rename src/{ => shared}/utils/debug.ts (100%) rename src/{ => shared}/utils/env-api-key.ts (100%) rename src/{ => shared}/utils/env-scan.ts (100%) rename src/{ => shared}/utils/environment.ts (100%) rename src/{ => shared}/utils/links.ts (100%) rename src/{ => shared}/utils/oauth-errors.ts (100%) rename src/{ => shared}/utils/oauth.ts (100%) rename src/{ => shared}/utils/package-json.ts (100%) rename src/{ => shared}/utils/package-manager.ts (99%) rename src/{ => shared}/utils/paths.ts (100%) rename src/{ => shared}/utils/project-resolution.ts (100%) rename src/{ => shared}/utils/provisioning.ts (100%) rename src/{ => shared}/utils/rules/astro-rules.md (100%) rename src/{ => shared}/utils/rules/next-rules.md (100%) rename src/{ => shared}/utils/rules/react-native-rules.md (100%) rename src/{ => shared}/utils/rules/react-rules.md (100%) rename src/{ => shared}/utils/rules/svelte-rules.md (100%) rename src/{ => shared}/utils/rules/universal.md (100%) rename src/{ => shared}/utils/semver.ts (100%) rename src/{ => shared}/utils/setup-utils.ts (99%) rename src/{ => shared}/utils/terminal-bell.ts (100%) rename src/{ => shared}/utils/types.ts (100%) rename src/{ => shared}/utils/urls.ts (100%) rename src/{ => shared}/utils/vendor/is-unicorn-supported.ts (100%) rename src/{ => shared}/utils/wizard-abort.ts (100%) diff --git a/e2e-tests/mocks/handlers.ts b/e2e-tests/mocks/handlers.ts index 5cf864edf..f940e9bb3 100644 --- a/e2e-tests/mocks/handlers.ts +++ b/e2e-tests/mocks/handlers.ts @@ -1,5 +1,5 @@ import { http, HttpResponse, passthrough } from 'msw'; -import { getCloudUrl } from '../../src/utils/urls'; +import { getCloudUrl } from '../../src/shared/utils/urls'; import { DEFAULT_HOST_URL } from '../../src/lib/constants'; import { fixtureTracker } from './fixture-tracker'; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index cac070705..9609fb5f2 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -70,8 +70,8 @@ "src/ui/tui/store.ts -> src/lib/agent/token-pricing.ts", "src/ui/wizard-ui.ts -> src/lib/agent/claude-settings.ts", "src/ui/wizard-ui.ts -> src/lib/agent/progress.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/shared/utils/package-manager.ts -> src/telemetry.ts", + "src/shared/utils/setup-utils.ts -> src/telemetry.ts", + "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts" ] } diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 4633470ee..03b79a595 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -59,7 +59,7 @@ vi.mock('../lib/wizard-session', async (importOriginal) => ({ ...(await importOriginal()), buildSession: mockBuildSessionCli, })); -vi.mock('../utils/provisioning', () => ({ +vi.mock('@utils/provisioning', () => ({ provisionNewAccount: mockProvisionNewAccountCli, })); vi.mock('../ui/tui/start-tui', () => ({ @@ -88,15 +88,15 @@ vi.mock('../lib/programs/posthog-integration/index', () => ({ run: () => Promise.resolve(), }, })); -vi.mock('../utils/environment', () => ({ +vi.mock('@utils/environment', () => ({ isNonInteractiveEnvironment: () => false, readEnvironment: () => ({}), })); // CI-path dynamic imports need mocks to prevent unhandled rejections -vi.mock('../utils/env-api-key', () => ({ +vi.mock('@utils/env-api-key', () => ({ readApiKeyFromEnv: () => undefined, })); -vi.mock('../utils/debug', () => ({ +vi.mock('@utils/debug', () => ({ configureLogFileFromEnvironment: vi.fn(), logToFile: vi.fn(), })); @@ -105,11 +105,11 @@ vi.mock('../lib/detection/index', () => ({ detectFramework: vi.fn().mockResolvedValue(null), gatherFrameworkContext: vi.fn().mockResolvedValue({}), })); -vi.mock('../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { setTag: vi.fn() }, })); -vi.mock('../utils/wizard-abort', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@utils/wizard-abort', async (importOriginal) => ({ + ...(await importOriginal()), wizardAbort: vi.fn(), })); vi.mock('../lib/programs/run-agent-legacy', () => ({ @@ -444,7 +444,7 @@ describe('CLI argument parsing', () => { '/tmp/test', ]); - const { analytics } = await import('../utils/analytics'); + const { analytics } = await import('@utils/analytics'); expect(analytics.setTag).toHaveBeenCalledWith('build', 'ci'); }); @@ -527,7 +527,7 @@ describe('CLI argument parsing', () => { '/tmp/test', ]); - const { analytics } = await import('../utils/analytics'); + const { analytics } = await import('@utils/analytics'); expect(analytics.setTag).toHaveBeenCalledWith('build', 'headless'); expect(analytics.setTag).not.toHaveBeenCalledWith('build', 'ci'); }); @@ -544,7 +544,7 @@ describe('CLI argument parsing', () => { '/tmp/test', ]); - const { analytics } = await import('../utils/analytics'); + const { analytics } = await import('@utils/analytics'); expect(analytics.setTag).toHaveBeenCalledWith('build', 'headless'); expect(analytics.setTag).not.toHaveBeenCalledWith('build', 'ci'); }); diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 95d547bd0..636f75f72 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -5,7 +5,7 @@ const { mockProvisionNewAccountSubcmd } = vi.hoisted(() => ({ })); vi.mock('semver', () => ({ satisfies: () => true })); -vi.mock('../utils/provisioning', () => ({ +vi.mock('@utils/provisioning', () => ({ provisionNewAccount: mockProvisionNewAccountSubcmd, })); // Same supporting mocks as src/__tests__/cli.test.ts — bin.ts imports these @@ -40,14 +40,14 @@ vi.mock('../lib/programs/posthog-integration/index', () => ({ run: () => Promise.resolve(), }, })); -vi.mock('../utils/environment', () => ({ +vi.mock('@utils/environment', () => ({ isNonInteractiveEnvironment: () => false, readEnvironment: () => ({}), })); -vi.mock('../utils/env-api-key', () => ({ +vi.mock('@utils/env-api-key', () => ({ readApiKeyFromEnv: () => undefined, })); -vi.mock('../utils/debug', () => ({ +vi.mock('@utils/debug', () => ({ configureLogFileFromEnvironment: vi.fn(), logToFile: vi.fn(), })); @@ -56,11 +56,11 @@ vi.mock('../lib/detection/index', () => ({ detectFramework: vi.fn().mockResolvedValue(null), gatherFrameworkContext: vi.fn().mockResolvedValue({}), })); -vi.mock('../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { setTag: vi.fn() }, })); -vi.mock('../utils/wizard-abort', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@utils/wizard-abort', async (importOriginal) => ({ + ...(await importOriginal()), wizardAbort: vi.fn(), })); vi.mock('../lib/programs/run-agent-legacy', () => ({ diff --git a/src/__tests__/wizard-abort.test.ts b/src/__tests__/wizard-abort.test.ts index 8699354c6..829157408 100644 --- a/src/__tests__/wizard-abort.test.ts +++ b/src/__tests__/wizard-abort.test.ts @@ -10,7 +10,7 @@ import { analytics } from '@utils/analytics'; import { ErrorCodes } from '@lib/errors'; import { getUI } from '../ui'; -vi.mock('../utils/analytics'); +vi.mock('@utils/analytics'); vi.mock('../ui', () => ({ getUI: vi.fn().mockReturnValue({ outroError: vi.fn(), diff --git a/src/lib/__tests__/agent-interface.test.ts b/src/lib/__tests__/agent-interface.test.ts index 7ca36c2ca..01f6fe3c9 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/lib/__tests__/agent-interface.test.ts @@ -21,8 +21,8 @@ import { } from '@lib/wizard-session'; // Mock dependencies -vi.mock('../../utils/analytics'); -vi.mock('../../utils/debug'); +vi.mock('@utils/analytics'); +vi.mock('@utils/debug'); // Mock the SDK module const mockQuery = vi.fn(); diff --git a/src/lib/__tests__/cloudflare-detection.test.ts b/src/lib/__tests__/cloudflare-detection.test.ts index 7879b5320..b7c33cf7e 100644 --- a/src/lib/__tests__/cloudflare-detection.test.ts +++ b/src/lib/__tests__/cloudflare-detection.test.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as os from 'os'; import { detectCloudflareTarget } from '@lib/cloudflare-detection'; -vi.mock('../../utils/debug'); +vi.mock('@utils/debug'); function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'cloudflare-detect-')); diff --git a/src/lib/__tests__/wizard-ask-bridge.test.ts b/src/lib/__tests__/wizard-ask-bridge.test.ts index 1f12b9a4c..4cb0383a8 100644 --- a/src/lib/__tests__/wizard-ask-bridge.test.ts +++ b/src/lib/__tests__/wizard-ask-bridge.test.ts @@ -6,7 +6,7 @@ import { import { analytics } from '@utils/analytics'; import type { AskAnswers, PendingQuestion } from '@lib/wizard-session'; -vi.mock('../../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn(), }, diff --git a/src/lib/__tests__/wizard-can-use-tool.test.ts b/src/lib/__tests__/wizard-can-use-tool.test.ts index 26272ea66..90960aeeb 100644 --- a/src/lib/__tests__/wizard-can-use-tool.test.ts +++ b/src/lib/__tests__/wizard-can-use-tool.test.ts @@ -1,11 +1,11 @@ import { wizardCanUseTool } from '@lib/agent/agent-interface'; -vi.mock('../../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn(), }, })); -vi.mock('../../utils/debug'); +vi.mock('@utils/debug'); describe('wizardCanUseTool — wizard_ask pending guard', () => { for (const tool of ['Write', 'Edit'] as const) { diff --git a/src/lib/__tests__/yara-hooks.test.ts b/src/lib/__tests__/yara-hooks.test.ts index c5167d547..239dbc5e3 100644 --- a/src/lib/__tests__/yara-hooks.test.ts +++ b/src/lib/__tests__/yara-hooks.test.ts @@ -12,11 +12,11 @@ import { import { scan, triageMatches } from '@posthog/warlock'; import fs from 'fs'; import fg from 'fast-glob'; -import * as analyticsModule from '../../utils/analytics'; +import * as analyticsModule from '@utils/analytics'; // Mock dependencies -vi.mock('../../utils/debug'); -vi.mock('../../utils/analytics'); +vi.mock('@utils/debug'); +vi.mock('@utils/analytics'); vi.mock('fs'); vi.mock('fast-glob'); diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index e45358fd1..d316989ca 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -12,10 +12,10 @@ import { OutroKind, type OutroData } from '@lib/agent/progress'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; -import { logToFile } from '../../../../utils/debug'; +import { logToFile } from '@utils/debug'; import { createBenchmarkPipeline } from '../../../middleware/benchmark'; import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@lib/errors'; -import { analytics } from '../../../../utils/analytics'; +import { analytics } from '@utils/analytics'; import { formatYaraAbortMessage } from '../../../yara-hooks'; import { installSkillById } from '../../../wizard-tools'; import { assemblePrompt } from '../../agent-prompt'; diff --git a/src/lib/detection/__tests__/package-manager.test.ts b/src/lib/detection/__tests__/package-manager.test.ts index e74e27733..3200bf4fd 100644 --- a/src/lib/detection/__tests__/package-manager.test.ts +++ b/src/lib/detection/__tests__/package-manager.test.ts @@ -14,11 +14,11 @@ import { pubPackageManager, } from '@lib/detection/package-manager'; -vi.mock('../../../utils/debug'); +vi.mock('@utils/debug'); vi.mock('../../../telemetry', () => ({ withProgress: (_name: string, fn: () => unknown) => fn(), })); -vi.mock('../../../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { setTag: vi.fn() }, })); diff --git a/src/utils/__fixtures__/provisioning.ts b/src/shared/utils/__fixtures__/provisioning.ts similarity index 100% rename from src/utils/__fixtures__/provisioning.ts rename to src/shared/utils/__fixtures__/provisioning.ts diff --git a/src/utils/__tests__/analytics.test.ts b/src/shared/utils/__tests__/analytics.test.ts similarity index 100% rename from src/utils/__tests__/analytics.test.ts rename to src/shared/utils/__tests__/analytics.test.ts diff --git a/src/utils/__tests__/bounded-fs-walk.test.ts b/src/shared/utils/__tests__/bounded-fs-walk.test.ts similarity index 100% rename from src/utils/__tests__/bounded-fs-walk.test.ts rename to src/shared/utils/__tests__/bounded-fs-walk.test.ts diff --git a/src/utils/__tests__/bounded-fs.test.ts b/src/shared/utils/__tests__/bounded-fs.test.ts similarity index 100% rename from src/utils/__tests__/bounded-fs.test.ts rename to src/shared/utils/__tests__/bounded-fs.test.ts diff --git a/src/utils/__tests__/ci-flag-overrides.test.ts b/src/shared/utils/__tests__/ci-flag-overrides.test.ts similarity index 100% rename from src/utils/__tests__/ci-flag-overrides.test.ts rename to src/shared/utils/__tests__/ci-flag-overrides.test.ts diff --git a/src/utils/__tests__/ci-region.test.ts b/src/shared/utils/__tests__/ci-region.test.ts similarity index 100% rename from src/utils/__tests__/ci-region.test.ts rename to src/shared/utils/__tests__/ci-region.test.ts diff --git a/src/utils/__tests__/debug.test.ts b/src/shared/utils/__tests__/debug.test.ts similarity index 100% rename from src/utils/__tests__/debug.test.ts rename to src/shared/utils/__tests__/debug.test.ts diff --git a/src/utils/__tests__/env-api-key.test.ts b/src/shared/utils/__tests__/env-api-key.test.ts similarity index 100% rename from src/utils/__tests__/env-api-key.test.ts rename to src/shared/utils/__tests__/env-api-key.test.ts diff --git a/src/utils/__tests__/env-scan.test.ts b/src/shared/utils/__tests__/env-scan.test.ts similarity index 100% rename from src/utils/__tests__/env-scan.test.ts rename to src/shared/utils/__tests__/env-scan.test.ts diff --git a/src/utils/__tests__/environment.test.ts b/src/shared/utils/__tests__/environment.test.ts similarity index 100% rename from src/utils/__tests__/environment.test.ts rename to src/shared/utils/__tests__/environment.test.ts diff --git a/src/utils/__tests__/links.test.ts b/src/shared/utils/__tests__/links.test.ts similarity index 100% rename from src/utils/__tests__/links.test.ts rename to src/shared/utils/__tests__/links.test.ts diff --git a/src/utils/__tests__/oauth-errors.test.ts b/src/shared/utils/__tests__/oauth-errors.test.ts similarity index 100% rename from src/utils/__tests__/oauth-errors.test.ts rename to src/shared/utils/__tests__/oauth-errors.test.ts diff --git a/src/utils/__tests__/oauth-refresh.test.ts b/src/shared/utils/__tests__/oauth-refresh.test.ts similarity index 100% rename from src/utils/__tests__/oauth-refresh.test.ts rename to src/shared/utils/__tests__/oauth-refresh.test.ts diff --git a/src/utils/__tests__/oauth-server.test.ts b/src/shared/utils/__tests__/oauth-server.test.ts similarity index 100% rename from src/utils/__tests__/oauth-server.test.ts rename to src/shared/utils/__tests__/oauth-server.test.ts diff --git a/src/utils/__tests__/oauth.test.ts b/src/shared/utils/__tests__/oauth.test.ts similarity index 100% rename from src/utils/__tests__/oauth.test.ts rename to src/shared/utils/__tests__/oauth.test.ts diff --git a/src/utils/__tests__/project-resolution.test.ts b/src/shared/utils/__tests__/project-resolution.test.ts similarity index 100% rename from src/utils/__tests__/project-resolution.test.ts rename to src/shared/utils/__tests__/project-resolution.test.ts diff --git a/src/utils/__tests__/provisioning.test.ts b/src/shared/utils/__tests__/provisioning.test.ts similarity index 100% rename from src/utils/__tests__/provisioning.test.ts rename to src/shared/utils/__tests__/provisioning.test.ts diff --git a/src/utils/__tests__/semver.test.ts b/src/shared/utils/__tests__/semver.test.ts similarity index 100% rename from src/utils/__tests__/semver.test.ts rename to src/shared/utils/__tests__/semver.test.ts diff --git a/src/utils/__tests__/terminal-bell.test.ts b/src/shared/utils/__tests__/terminal-bell.test.ts similarity index 100% rename from src/utils/__tests__/terminal-bell.test.ts rename to src/shared/utils/__tests__/terminal-bell.test.ts diff --git a/src/utils/analytics.ts b/src/shared/utils/analytics.ts similarity index 100% rename from src/utils/analytics.ts rename to src/shared/utils/analytics.ts diff --git a/src/utils/atomic-ledger.ts b/src/shared/utils/atomic-ledger.ts similarity index 100% rename from src/utils/atomic-ledger.ts rename to src/shared/utils/atomic-ledger.ts diff --git a/src/utils/bounded-fs.ts b/src/shared/utils/bounded-fs.ts similarity index 100% rename from src/utils/bounded-fs.ts rename to src/shared/utils/bounded-fs.ts diff --git a/src/utils/ci-flag-overrides.ts b/src/shared/utils/ci-flag-overrides.ts similarity index 100% rename from src/utils/ci-flag-overrides.ts rename to src/shared/utils/ci-flag-overrides.ts diff --git a/src/utils/clipboard.ts b/src/shared/utils/clipboard.ts similarity index 100% rename from src/utils/clipboard.ts rename to src/shared/utils/clipboard.ts diff --git a/src/utils/custom-headers.ts b/src/shared/utils/custom-headers.ts similarity index 100% rename from src/utils/custom-headers.ts rename to src/shared/utils/custom-headers.ts diff --git a/src/utils/debug.ts b/src/shared/utils/debug.ts similarity index 100% rename from src/utils/debug.ts rename to src/shared/utils/debug.ts diff --git a/src/utils/env-api-key.ts b/src/shared/utils/env-api-key.ts similarity index 100% rename from src/utils/env-api-key.ts rename to src/shared/utils/env-api-key.ts diff --git a/src/utils/env-scan.ts b/src/shared/utils/env-scan.ts similarity index 100% rename from src/utils/env-scan.ts rename to src/shared/utils/env-scan.ts diff --git a/src/utils/environment.ts b/src/shared/utils/environment.ts similarity index 100% rename from src/utils/environment.ts rename to src/shared/utils/environment.ts diff --git a/src/utils/links.ts b/src/shared/utils/links.ts similarity index 100% rename from src/utils/links.ts rename to src/shared/utils/links.ts diff --git a/src/utils/oauth-errors.ts b/src/shared/utils/oauth-errors.ts similarity index 100% rename from src/utils/oauth-errors.ts rename to src/shared/utils/oauth-errors.ts diff --git a/src/utils/oauth.ts b/src/shared/utils/oauth.ts similarity index 100% rename from src/utils/oauth.ts rename to src/shared/utils/oauth.ts diff --git a/src/utils/package-json.ts b/src/shared/utils/package-json.ts similarity index 100% rename from src/utils/package-json.ts rename to src/shared/utils/package-json.ts diff --git a/src/utils/package-manager.ts b/src/shared/utils/package-manager.ts similarity index 99% rename from src/utils/package-manager.ts rename to src/shared/utils/package-manager.ts index 2c3790fda..dc846f275 100644 --- a/src/utils/package-manager.ts +++ b/src/shared/utils/package-manager.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { readFileHead } from './bounded-fs'; -import { withProgress } from '../telemetry'; +import { withProgress } from '../../telemetry'; import { getPackageDotJson, updatePackageDotJson } from './setup-utils'; import type { PackageJson } from './package-json'; import { analytics } from './analytics'; diff --git a/src/utils/paths.ts b/src/shared/utils/paths.ts similarity index 100% rename from src/utils/paths.ts rename to src/shared/utils/paths.ts diff --git a/src/utils/project-resolution.ts b/src/shared/utils/project-resolution.ts similarity index 100% rename from src/utils/project-resolution.ts rename to src/shared/utils/project-resolution.ts diff --git a/src/utils/provisioning.ts b/src/shared/utils/provisioning.ts similarity index 100% rename from src/utils/provisioning.ts rename to src/shared/utils/provisioning.ts diff --git a/src/utils/rules/astro-rules.md b/src/shared/utils/rules/astro-rules.md similarity index 100% rename from src/utils/rules/astro-rules.md rename to src/shared/utils/rules/astro-rules.md diff --git a/src/utils/rules/next-rules.md b/src/shared/utils/rules/next-rules.md similarity index 100% rename from src/utils/rules/next-rules.md rename to src/shared/utils/rules/next-rules.md diff --git a/src/utils/rules/react-native-rules.md b/src/shared/utils/rules/react-native-rules.md similarity index 100% rename from src/utils/rules/react-native-rules.md rename to src/shared/utils/rules/react-native-rules.md diff --git a/src/utils/rules/react-rules.md b/src/shared/utils/rules/react-rules.md similarity index 100% rename from src/utils/rules/react-rules.md rename to src/shared/utils/rules/react-rules.md diff --git a/src/utils/rules/svelte-rules.md b/src/shared/utils/rules/svelte-rules.md similarity index 100% rename from src/utils/rules/svelte-rules.md rename to src/shared/utils/rules/svelte-rules.md diff --git a/src/utils/rules/universal.md b/src/shared/utils/rules/universal.md similarity index 100% rename from src/utils/rules/universal.md rename to src/shared/utils/rules/universal.md diff --git a/src/utils/semver.ts b/src/shared/utils/semver.ts similarity index 100% rename from src/utils/semver.ts rename to src/shared/utils/semver.ts diff --git a/src/utils/setup-utils.ts b/src/shared/utils/setup-utils.ts similarity index 99% rename from src/utils/setup-utils.ts rename to src/shared/utils/setup-utils.ts index 880fdd8b9..89a8c9ee5 100644 --- a/src/utils/setup-utils.ts +++ b/src/shared/utils/setup-utils.ts @@ -4,7 +4,7 @@ import * as os from 'node:os'; import { basename, isAbsolute, join, relative } from 'node:path'; import { promisify } from 'node:util'; -import { withProgress } from '../telemetry'; +import { withProgress } from '../../telemetry'; import { debug, logToFile } from './debug'; import type { PackageJson } from './package-json'; import { diff --git a/src/utils/terminal-bell.ts b/src/shared/utils/terminal-bell.ts similarity index 100% rename from src/utils/terminal-bell.ts rename to src/shared/utils/terminal-bell.ts diff --git a/src/utils/types.ts b/src/shared/utils/types.ts similarity index 100% rename from src/utils/types.ts rename to src/shared/utils/types.ts diff --git a/src/utils/urls.ts b/src/shared/utils/urls.ts similarity index 100% rename from src/utils/urls.ts rename to src/shared/utils/urls.ts diff --git a/src/utils/vendor/is-unicorn-supported.ts b/src/shared/utils/vendor/is-unicorn-supported.ts similarity index 100% rename from src/utils/vendor/is-unicorn-supported.ts rename to src/shared/utils/vendor/is-unicorn-supported.ts diff --git a/src/utils/wizard-abort.ts b/src/shared/utils/wizard-abort.ts similarity index 100% rename from src/utils/wizard-abort.ts rename to src/shared/utils/wizard-abort.ts diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts index 4af8c4e36..9623f1851 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts @@ -10,11 +10,11 @@ vi.mock('fs', () => ({ existsSync: vi.fn().mockReturnValue(false), })); -vi.mock('../../../../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { captureException: vi.fn() }, })); -vi.mock('../../../../utils/debug', () => ({ +vi.mock('@utils/debug', () => ({ debug: vi.fn(), })); diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts index 4ff93c361..d497a16a8 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/codex.test.ts @@ -14,7 +14,7 @@ vi.mock('node:fs', () => ({ rmSync: vi.fn(), })); -vi.mock('../../../../utils/analytics', () => ({ +vi.mock('@utils/analytics', () => ({ analytics: { captureException: vi.fn() }, })); diff --git a/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts b/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts index 19a0b9c8f..b87d0cbc9 100644 --- a/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts +++ b/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts @@ -12,7 +12,7 @@ import { installSteeringSnippet, } from '@steps/install-cli-steering'; -vi.mock('../../../utils/debug'); +vi.mock('@utils/debug'); vi.mock('node:child_process', () => ({ spawnSync: vi.fn(), })); diff --git a/src/ui/tui/__tests__/WizardAskScreen.test.ts b/src/ui/tui/__tests__/WizardAskScreen.test.ts index b2b04a695..5938bfc17 100644 --- a/src/ui/tui/__tests__/WizardAskScreen.test.ts +++ b/src/ui/tui/__tests__/WizardAskScreen.test.ts @@ -9,7 +9,7 @@ import { vi } from 'vitest'; -vi.mock('../../../utils/analytics.js', () => ({ +vi.mock('@utils/analytics.js', () => ({ analytics: { capture: vi.fn(), wizardCapture: vi.fn(), diff --git a/src/ui/tui/__tests__/exit-line.test.ts b/src/ui/tui/__tests__/exit-line.test.ts index f2ee5bc28..05db612ca 100644 --- a/src/ui/tui/__tests__/exit-line.test.ts +++ b/src/ui/tui/__tests__/exit-line.test.ts @@ -3,7 +3,7 @@ import { WizardStore, Program } from '@ui/tui/store'; import { OutroKind } from '@lib/wizard-session'; import { HostResolution } from '@lib/host-resolution'; -vi.mock('../../../utils/analytics.js', () => ({ +vi.mock('@utils/analytics.js', () => ({ analytics: { capture: vi.fn(), wizardCapture: vi.fn(), diff --git a/src/ui/tui/__tests__/mcp-installer.test.ts b/src/ui/tui/__tests__/mcp-installer.test.ts index 200289286..1cb41f008 100644 --- a/src/ui/tui/__tests__/mcp-installer.test.ts +++ b/src/ui/tui/__tests__/mcp-installer.test.ts @@ -20,11 +20,11 @@ vi.mock('../../../steps/add-mcp-server-to-clients/defaults.js', () => ({ ALL_FEATURE_VALUES: ['feature-a'], })); -vi.mock('../../../utils/debug.js', () => ({ +vi.mock('@utils/debug.js', () => ({ logToFile: vi.fn(), })); -vi.mock('../../../utils/analytics.js', () => ({ +vi.mock('@utils/analytics.js', () => ({ analytics: { wizardCapture: vi.fn() }, })); diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts index 8977738e7..509b030bb 100644 --- a/src/ui/tui/__tests__/store-invariants.test.ts +++ b/src/ui/tui/__tests__/store-invariants.test.ts @@ -34,7 +34,7 @@ import { analytics } from '@utils/analytics'; import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; import type { SettingsConflict } from '@lib/agent/claude-settings'; -vi.mock('../../../utils/analytics.js', () => ({ +vi.mock('@utils/analytics.js', () => ({ analytics: { capture: vi.fn(), wizardCapture: vi.fn(), diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts index acaf7dc91..fbae262d6 100644 --- a/src/ui/tui/__tests__/store.test.ts +++ b/src/ui/tui/__tests__/store.test.ts @@ -20,7 +20,7 @@ import { Integration } from '@lib/constants'; import { analytics } from '@utils/analytics'; import { getProgramConfig } from '@lib/programs/program-registry'; -vi.mock('../../../utils/analytics.js', () => ({ +vi.mock('@utils/analytics.js', () => ({ analytics: { capture: vi.fn(), wizardCapture: vi.fn(), diff --git a/src/ui/tui/__tests__/task-notice.test.ts b/src/ui/tui/__tests__/task-notice.test.ts index 94be3f76c..8534f340a 100644 --- a/src/ui/tui/__tests__/task-notice.test.ts +++ b/src/ui/tui/__tests__/task-notice.test.ts @@ -5,7 +5,7 @@ */ import type { TaskNotice } from '@lib/wizard-session'; -vi.mock('../../../utils/analytics.js', () => ({ +vi.mock('@utils/analytics.js', () => ({ analytics: { capture: vi.fn(), wizardCapture: vi.fn(), diff --git a/tsconfig.build.json b/tsconfig.build.json index 8618277c1..3457f5ffe 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,7 +21,7 @@ "@env": ["./src/env.ts"], "@lib/*": ["./src/lib/*"], "@e2e-harness/*": ["./e2e-harness/*"], - "@utils/*": ["./src/utils/*"], + "@utils/*": ["./src/shared/utils/*"], "@ui": ["./src/ui/index.ts"], "@ui/*": ["./src/ui/*"], "@steps": ["./src/steps/index.ts"], diff --git a/vitest.config.ts b/vitest.config.ts index 7985ed29d..af7eabb86 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -89,7 +89,7 @@ export default defineConfig({ { find: /^@env$/, replacement: r('src/env.ts') }, { find: /^@lib\/(.*)$/, replacement: `${r('src/lib')}/$1` }, { find: /^@e2e-harness\/(.*)$/, replacement: `${r('e2e-harness')}/$1` }, - { find: /^@utils\/(.*)$/, replacement: `${r('src/utils')}/$1` }, + { find: /^@utils\/(.*)$/, replacement: `${r('src/shared/utils')}/$1` }, { find: /^@ui$/, replacement: r('src/ui/index.ts') }, { find: /^@ui\/(.*)$/, replacement: `${r('src/ui')}/$1` }, { find: /^@steps$/, replacement: r('src/steps/index.ts') }, From 8210a64745e9717a28be8af533cfcc9ec0b90c9a Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 00:17:18 -0400 Subject: [PATCH 21/61] refactor(shared): move library utilities and health checks into shared Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .gitignore | 2 +- bin.ts | 4 ++-- e2e-harness/__tests__/e2e-flow-snapshot.test.ts | 6 +++--- e2e-harness/__tests__/wizard-ci-driver.test.ts | 6 +++--- e2e-tests/mocks/handlers.ts | 2 +- scripts/generate-version.cjs | 2 +- scripts/tui-host.no-jest.ts | 8 ++++---- src/__tests__/architecture/known-violations.json | 2 +- src/__tests__/cli.test.ts | 2 +- src/__tests__/wizard-abort.test.ts | 2 +- src/commands/basic-integration/ci-install.ts | 4 ++-- src/commands/basic-integration/non-interactive.ts | 4 ++-- src/commands/basic-integration/playground.ts | 2 +- src/commands/basic-integration/skill.ts | 2 +- src/commands/doctor.ts | 6 +++--- src/commands/factories/family-command-factory.ts | 2 +- src/commands/factories/shared.ts | 4 ++-- src/commands/mcp/add.ts | 2 +- src/commands/mcp/remove.ts | 2 +- src/commands/mcp/tutorial.ts | 6 +++--- src/commands/skill.ts | 6 +++--- src/commands/slack.ts | 6 +++--- src/frameworks/android/android-wizard-agent.ts | 2 +- src/frameworks/angular/angular-wizard-agent.ts | 2 +- src/frameworks/astro/astro-wizard-agent.ts | 2 +- src/frameworks/django/django-wizard-agent.ts | 2 +- src/frameworks/elixir/elixir-wizard-agent.ts | 2 +- src/frameworks/fastapi/fastapi-wizard-agent.ts | 2 +- src/frameworks/flask/flask-wizard-agent.ts | 2 +- src/frameworks/flutter/flutter-wizard-agent.ts | 2 +- src/frameworks/go/go-wizard-agent.ts | 2 +- src/frameworks/java/java-wizard-agent.ts | 2 +- .../javascript-node/javascript-node-wizard-agent.ts | 2 +- .../javascript-web/javascript-web-wizard-agent.ts | 2 +- src/frameworks/kmp/kmp-wizard-agent.ts | 2 +- src/frameworks/laravel/laravel-wizard-agent.ts | 2 +- src/frameworks/nextjs/nextjs-wizard-agent.ts | 2 +- src/frameworks/nuxt/nuxt-wizard-agent.ts | 2 +- src/frameworks/python/python-wizard-agent.ts | 2 +- src/frameworks/rails/rails-wizard-agent.ts | 2 +- .../react-native/react-native-wizard-agent.ts | 2 +- .../react-router/react-router-wizard-agent.ts | 2 +- src/frameworks/ruby/ruby-wizard-agent.ts | 2 +- src/frameworks/rust/rust-wizard-agent.ts | 2 +- src/frameworks/svelte/svelte-wizard-agent.ts | 2 +- src/frameworks/swift/swift-wizard-agent.ts | 2 +- .../tanstack-router/tanstack-router-wizard-agent.ts | 2 +- .../tanstack-start/tanstack-start-wizard-agent.ts | 2 +- src/frameworks/vue/vue-wizard-agent.ts | 2 +- src/lib/__tests__/agent-interface.test.ts | 2 +- src/lib/__tests__/gateway-session.test.ts | 10 +++++----- src/lib/__tests__/mcp-role-prompts.test.ts | 2 +- src/lib/__tests__/wizard-spellbook.test.ts | 2 +- src/lib/__tests__/yara-hooks.test.ts | 2 +- src/lib/agent/__tests__/agent-prompt-loader.test.ts | 2 +- src/lib/agent/__tests__/agent-prompt.test.ts | 2 +- src/lib/agent/__tests__/commandments.test.ts | 2 +- src/lib/agent/__tests__/progress-collector.test.ts | 2 +- src/lib/agent/__tests__/run-agent-standalone.test.ts | 6 +++--- src/lib/agent/__tests__/run-tags.test.ts | 2 +- src/lib/agent/__tests__/triage-provider.test.ts | 2 +- src/lib/agent/agent-interface.ts | 10 +++++----- src/lib/agent/agent-prompt-loader.ts | 4 ++-- src/lib/agent/agent-prompt.ts | 2 +- src/lib/agent/aio-capture.ts | 2 +- src/lib/agent/mcp-prompt-streaming.ts | 4 ++-- src/lib/agent/progress.ts | 2 +- src/lib/agent/runner/__tests__/switchboard.test.ts | 2 +- .../anthropic/__tests__/pending-question.test.ts | 4 ++-- src/lib/agent/runner/harness/anthropic/index.ts | 2 +- src/lib/agent/runner/harness/pi/index.ts | 2 +- src/lib/agent/runner/harness/pi/mcp.ts | 2 +- src/lib/agent/runner/harness/pi/runtime-notes.ts | 2 +- src/lib/agent/runner/harness/pi/task.ts | 2 +- src/lib/agent/runner/harness/pi/tools.ts | 4 ++-- src/lib/agent/runner/harness/types.ts | 4 ++-- src/lib/agent/runner/index.ts | 4 ++-- src/lib/agent/runner/sequence/linear.ts | 2 +- .../orchestrator/__tests__/variant-resolution.test.ts | 2 +- .../sequence/orchestrator/orchestrator-runner.ts | 4 ++-- src/lib/agent/runner/shared/bootstrap.ts | 6 +++--- src/lib/agent/runner/shared/errors.ts | 4 ++-- src/lib/agent/runner/shared/progress-collector.ts | 2 +- src/lib/agent/runner/shared/types.ts | 10 +++++----- src/lib/agent/runner/switchboard/commandments.ts | 2 +- .../switchboard/flags/__tests__/binding-cases.ts | 2 +- .../runner/switchboard/flags/__tests__/flags.test.ts | 4 ++-- src/lib/agent/runner/switchboard/flags/index.ts | 2 +- src/lib/agent/runner/switchboard/flags/orchestrator.ts | 2 +- src/lib/agent/runner/switchboard/flags/schemes.ts | 2 +- src/lib/agent/runner/switchboard/flags/self-driving.ts | 2 +- src/lib/agent/runner/switchboard/harness.ts | 2 +- src/lib/agent/runner/switchboard/index.ts | 2 +- src/lib/agent/runner/switchboard/models.ts | 2 +- src/lib/agent/runner/switchboard/sequence.ts | 2 +- src/lib/agent/token-pricing.ts | 2 +- src/lib/agent/triage-provider.ts | 2 +- src/lib/detection/__tests__/agentic-progress.test.ts | 2 +- src/lib/detection/__tests__/framework.test.ts | 2 +- src/lib/detection/__tests__/project-scope.test.ts | 2 +- src/lib/detection/agentic.ts | 2 +- src/lib/detection/context.ts | 2 +- src/lib/detection/framework.ts | 2 +- src/lib/detection/project-scope.ts | 2 +- src/lib/framework-config.ts | 2 +- src/lib/gateway-session.ts | 8 ++++---- src/lib/mcp-project-profile.ts | 2 +- src/lib/mcp-role-prompts.ts | 2 +- src/lib/oauth/program-scopes.ts | 2 +- src/lib/programs/__tests__/agent-skill.test.ts | 2 +- src/lib/programs/__tests__/error-tracking.test.ts | 4 ++-- src/lib/programs/__tests__/flow-traces.test.ts | 6 +++--- .../__tests__/refresh-access-token-if-needed.test.ts | 5 ++++- src/lib/programs/__tests__/replay-vision.test.ts | 2 +- src/lib/programs/__tests__/run-agent-legacy.test.ts | 8 ++++---- src/lib/programs/__tests__/self-driving-detect.test.ts | 2 +- src/lib/programs/__tests__/self-driving-prompt.test.ts | 2 +- src/lib/programs/audit/detect.ts | 2 +- src/lib/programs/audit/types.ts | 2 +- src/lib/programs/authenticate.ts | 2 +- src/lib/programs/dispatch-family.ts | 6 +++--- .../error-tracking-upload-source-maps/detect.ts | 2 +- src/lib/programs/error-tracking/detect-agentic.ts | 2 +- src/lib/programs/error-tracking/index.ts | 4 ++-- src/lib/programs/events-audit/constants.ts | 2 +- src/lib/programs/mcp-analytics/index.ts | 2 +- src/lib/programs/posthog-doctor/fetch.ts | 4 ++-- src/lib/programs/posthog-doctor/kind-metadata.ts | 2 +- src/lib/programs/posthog-integration/constants.ts | 2 +- src/lib/programs/posthog-integration/index.ts | 6 +++--- src/lib/programs/program-registry.ts | 2 +- src/lib/programs/program-step.ts | 4 ++-- src/lib/programs/replay-vision/index.ts | 4 ++-- src/lib/programs/run-agent-legacy.ts | 6 +++--- src/lib/programs/self-driving/detect-agentic.ts | 2 +- src/lib/programs/self-driving/detect.ts | 2 +- src/lib/programs/shared/health-check-step.ts | 2 +- src/lib/programs/web-analytics-doctor/detect.ts | 2 +- src/lib/registry.ts | 2 +- src/lib/runners/__tests__/mint-recovery.test.ts | 6 +++--- src/lib/runners/run-non-interactive.ts | 10 +++++++--- src/lib/runners/run-wizard.ts | 8 ++++---- .../task-stream/__tests__/posthog-destination.test.ts | 2 +- src/lib/task-stream/task-stream-push.ts | 2 +- src/lib/wizard-session.ts | 8 ++++---- src/lib/wizard-spellbook.ts | 2 +- src/lib/wizard-tools/mcp.ts | 4 ++-- src/lib/wizard-tools/tools.ts | 6 +++--- src/lib/yara-hooks.ts | 6 +++--- src/{lib => shared}/__tests__/constants.test.ts | 5 ++++- src/{lib => shared}/__tests__/fetch-retry.test.ts | 0 src/{lib => shared}/__tests__/host-resolution.test.ts | 4 ++-- src/{lib => shared}/__tests__/local-dev.test.ts | 4 ++-- src/{lib => shared}/__tests__/secret-vault.test.ts | 0 src/{lib => shared}/api.ts | 0 src/{lib => shared}/audit-ledger.ts | 0 src/{lib => shared}/auth-session-state.ts | 0 src/{lib => shared}/constants.ts | 0 src/{lib => shared}/errors/__tests__/auth.test.ts | 0 src/{lib => shared}/errors/__tests__/codes.test.ts | 0 .../errors/__tests__/detect-map.test.ts | 0 .../errors/__tests__/run-failure.test.ts | 0 src/{lib => shared}/errors/__tests__/sanitize.test.ts | 0 src/{lib => shared}/errors/agent-map.ts | 2 +- src/{lib => shared}/errors/auth.ts | 0 src/{lib => shared}/errors/catalog.ts | 0 src/{lib => shared}/errors/codes.ts | 0 src/{lib => shared}/errors/detect-map.ts | 0 src/{lib => shared}/errors/emit.ts | 0 src/{lib => shared}/errors/index.ts | 0 src/{lib => shared}/errors/run-failure.ts | 0 src/{lib => shared}/errors/sanitize.ts | 0 src/{lib => shared}/errors/skill-map.ts | 0 src/{lib => shared}/errors/types.ts | 0 src/{lib => shared}/errors/wizard-error.ts | 0 src/{lib => shared}/fetch-retry.ts | 0 .../health-checks/__tests__/health-checks.test.ts | 6 +++--- src/{lib => shared}/health-checks/endpoints.ts | 2 +- src/{lib => shared}/health-checks/index.ts | 0 src/{lib => shared}/health-checks/readiness.ts | 0 src/{lib => shared}/health-checks/testme.md | 0 src/{lib => shared}/health-checks/types.ts | 0 src/{lib => shared}/host-resolution.ts | 2 +- src/{lib => shared}/local-dev.ts | 0 src/{lib => shared}/secret-vault.ts | 0 src/{lib => shared}/skill-install.ts | 0 src/{lib => shared}/status-history.ts | 0 src/shared/utils/__tests__/analytics.test.ts | 6 +++--- src/shared/utils/__tests__/ci-region.test.ts | 4 ++-- src/shared/utils/__tests__/oauth-errors.test.ts | 2 +- src/shared/utils/__tests__/oauth-refresh.test.ts | 2 +- src/shared/utils/__tests__/oauth.test.ts | 2 +- src/shared/utils/analytics.ts | 6 +++--- src/shared/utils/environment.ts | 2 +- src/shared/utils/oauth-errors.ts | 2 +- src/shared/utils/oauth.ts | 2 +- src/shared/utils/provisioning.ts | 4 ++-- src/shared/utils/setup-utils.ts | 6 +++--- src/shared/utils/types.ts | 2 +- src/shared/utils/urls.ts | 2 +- src/shared/utils/wizard-abort.ts | 8 ++++++-- src/steps/add-mcp-server-to-clients/index.ts | 2 +- src/steps/add-or-update-environment-variables.ts | 2 +- src/steps/run-prettier.ts | 2 +- src/steps/upload-environment-variables/index.ts | 2 +- src/ui/logging-ui.ts | 6 +++--- src/ui/tui/__tests__/MintFailureScreen.test.tsx | 2 +- src/ui/tui/__tests__/exit-line.test.ts | 2 +- src/ui/tui/__tests__/frames.test.tsx | 10 +++++----- src/ui/tui/__tests__/keyboard-equivalence.test.tsx | 10 +++++----- src/ui/tui/__tests__/programs.test.ts | 2 +- src/ui/tui/__tests__/router.test.ts | 6 +++--- src/ui/tui/__tests__/store-invariants.test.ts | 8 ++++---- src/ui/tui/__tests__/store.test.ts | 8 ++++---- src/ui/tui/components/PrivacyPanel.tsx | 2 +- src/ui/tui/components/ServiceHealthList.tsx | 4 ++-- src/ui/tui/constants.ts | 2 +- src/ui/tui/hooks/__tests__/useGithubConnection.test.ts | 2 +- src/ui/tui/hooks/useGithubConnection.ts | 2 +- src/ui/tui/ink-ui.ts | 4 ++-- src/ui/tui/playground/demos/AiOptInDemo.tsx | 2 +- src/ui/tui/playground/demos/EndScreensDemo.tsx | 2 +- src/ui/tui/playground/demos/HealthCheckDemo.tsx | 6 +++--- .../tui/playground/demos/McpSuggestedPromptsDemo.tsx | 4 ++-- src/ui/tui/playground/start-playground.ts | 4 ++-- src/ui/tui/screens/AiOptInRequiredScreen.tsx | 4 ++-- src/ui/tui/screens/KeepSkillsScreen.tsx | 2 +- src/ui/tui/screens/McpSuggestedPromptsScreen.tsx | 2 +- src/ui/tui/screens/PortConflictScreen.tsx | 2 +- src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx | 2 +- .../tui/screens/SelfDrivingIntegrationCheckScreen.tsx | 2 +- .../tui/screens/SelfDrivingIntegrationDetectScreen.tsx | 2 +- src/ui/tui/screens/SessionTimeoutScreen.tsx | 2 +- src/ui/tui/screens/SkillSourceInfo.tsx | 2 +- src/ui/tui/screens/SlackConnectScreen.tsx | 2 +- src/ui/tui/screens/doctor/DoctorReportScreen.tsx | 4 ++-- src/ui/tui/screens/health/HealthCheckScreen.tsx | 8 ++++---- src/ui/tui/services/mcp-suggested-prompts-services.ts | 2 +- src/ui/tui/store.ts | 6 +++--- src/ui/wizard-ui.ts | 4 ++-- src/wizard.ts | 8 ++++---- tsconfig.build.json | 1 + vitest.config.ts | 1 + 243 files changed, 352 insertions(+), 336 deletions(-) rename src/{lib => shared}/__tests__/constants.test.ts (88%) rename src/{lib => shared}/__tests__/fetch-retry.test.ts (100%) rename src/{lib => shared}/__tests__/host-resolution.test.ts (97%) rename src/{lib => shared}/__tests__/local-dev.test.ts (99%) rename src/{lib => shared}/__tests__/secret-vault.test.ts (100%) rename src/{lib => shared}/api.ts (100%) rename src/{lib => shared}/audit-ledger.ts (100%) rename src/{lib => shared}/auth-session-state.ts (100%) rename src/{lib => shared}/constants.ts (100%) rename src/{lib => shared}/errors/__tests__/auth.test.ts (100%) rename src/{lib => shared}/errors/__tests__/codes.test.ts (100%) rename src/{lib => shared}/errors/__tests__/detect-map.test.ts (100%) rename src/{lib => shared}/errors/__tests__/run-failure.test.ts (100%) rename src/{lib => shared}/errors/__tests__/sanitize.test.ts (100%) rename src/{lib => shared}/errors/agent-map.ts (92%) rename src/{lib => shared}/errors/auth.ts (100%) rename src/{lib => shared}/errors/catalog.ts (100%) rename src/{lib => shared}/errors/codes.ts (100%) rename src/{lib => shared}/errors/detect-map.ts (100%) rename src/{lib => shared}/errors/emit.ts (100%) rename src/{lib => shared}/errors/index.ts (100%) rename src/{lib => shared}/errors/run-failure.ts (100%) rename src/{lib => shared}/errors/sanitize.ts (100%) rename src/{lib => shared}/errors/skill-map.ts (100%) rename src/{lib => shared}/errors/types.ts (100%) rename src/{lib => shared}/errors/wizard-error.ts (100%) rename src/{lib => shared}/fetch-retry.ts (100%) rename src/{lib => shared}/health-checks/__tests__/health-checks.test.ts (98%) rename src/{lib => shared}/health-checks/endpoints.ts (98%) rename src/{lib => shared}/health-checks/index.ts (100%) rename src/{lib => shared}/health-checks/readiness.ts (100%) rename src/{lib => shared}/health-checks/testme.md (100%) rename src/{lib => shared}/health-checks/types.ts (100%) rename src/{lib => shared}/host-resolution.ts (99%) rename src/{lib => shared}/local-dev.ts (100%) rename src/{lib => shared}/secret-vault.ts (100%) rename src/{lib => shared}/skill-install.ts (100%) rename src/{lib => shared}/status-history.ts (100%) diff --git a/.gitignore b/.gitignore index 3519c1fda..4ff37b6f0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,7 @@ plugins e2e-tests/fixtures/.tracking/* # Generated at build time by scripts/generate-version.js -src/lib/version.ts +src/shared/version.ts # Re-include src/lib explicitly: a user-global ignore pattern like `lib/` # would otherwise silently drop new source files under src/lib from commits. !src/lib/ diff --git a/bin.ts b/bin.ts index 2a3d25974..0c369d380 100644 --- a/bin.ts +++ b/bin.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import { satisfies } from 'semver'; import { Agent, setGlobalDispatcher } from 'undici'; -import { ErrorCodes } from './src/lib/errors/codes.js'; -import { emitWizardError } from './src/lib/errors/emit.js'; +import { ErrorCodes } from '@shared/errors/codes.js'; +import { emitWizardError } from '@shared/errors/emit.js'; // Keep in sync with `engines.node` in package.json. npx does not enforce // engines, so this preflight is the only thing standing between an old Node diff --git a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts index 243d4f830..272105bc6 100644 --- a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts +++ b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts @@ -17,10 +17,10 @@ import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui/index'; import { buildSession, RunPhase } from '@lib/wizard-session'; -import { Integration } from '@lib/constants'; -import { HostResolution } from '@lib/host-resolution'; +import { Integration } from '@shared/constants'; +import { HostResolution } from '@shared/host-resolution'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { Program, getProgramConfig, diff --git a/e2e-harness/__tests__/wizard-ci-driver.test.ts b/e2e-harness/__tests__/wizard-ci-driver.test.ts index 2edf4553e..7462b213f 100644 --- a/e2e-harness/__tests__/wizard-ci-driver.test.ts +++ b/e2e-harness/__tests__/wizard-ci-driver.test.ts @@ -13,10 +13,10 @@ import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui/index'; import { buildSession, RunPhase, McpOutcome } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; -import { Integration } from '@lib/constants'; +import { HostResolution } from '@shared/host-resolution'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { ScreenId, Overlay } from '@ui/tui/router'; import { Program } from '@lib/programs/program-registry'; import { WizardCiDriver, UnknownActionError } from '../wizard-ci-driver'; diff --git a/e2e-tests/mocks/handlers.ts b/e2e-tests/mocks/handlers.ts index f940e9bb3..68cd627b4 100644 --- a/e2e-tests/mocks/handlers.ts +++ b/e2e-tests/mocks/handlers.ts @@ -1,6 +1,6 @@ import { http, HttpResponse, passthrough } from 'msw'; import { getCloudUrl } from '../../src/shared/utils/urls'; -import { DEFAULT_HOST_URL } from '../../src/lib/constants'; +import { DEFAULT_HOST_URL } from '../../src/shared/constants'; import { fixtureTracker } from './fixture-tracker'; export const shouldRecord = process.env.RECORD_FIXTURES === 'true'; diff --git a/scripts/generate-version.cjs b/scripts/generate-version.cjs index 486932d39..3575dfe28 100644 --- a/scripts/generate-version.cjs +++ b/scripts/generate-version.cjs @@ -6,6 +6,6 @@ const pkg = JSON.parse( ); const output = `// Auto-generated by scripts/generate-version.js — do not edit\nexport const VERSION = '${pkg.version}';\n`; fs.writeFileSync( - path.join(__dirname, '..', 'src', 'lib', 'version.ts'), + path.join(__dirname, '..', 'src', 'shared', 'version.ts'), output, ); diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 61ca921bf..7dbcb690b 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -17,15 +17,15 @@ import fs from 'fs'; import net from 'net'; import { spawnSync } from 'child_process'; import { startTUI } from '@ui/tui/start-tui'; -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import { Program, getProgramConfig, type ProgramId, } from '@lib/programs/program-registry'; -import type { Harness, Sequence } from '@lib/constants'; +import type { Harness, Sequence } from '@shared/constants'; import { buildSession } from '@lib/wizard-session'; -import { initLocalDev } from '@lib/local-dev'; +import { initLocalDev } from '@shared/local-dev'; import { configureGatewayFromCIEnvironment } from '@lib/gateway-session'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; @@ -36,7 +36,7 @@ import { logToFile } from '@utils/debug'; import { join } from 'path'; import { detectFramework } from '@lib/detection/index'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; import { ERROR_TRACKING_PROJECT_PATH_KEY } from '@lib/programs/error-tracking/detect-agentic'; import { diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 9609fb5f2..9727fe328 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -3,7 +3,7 @@ "src/commands/factories/family-picker.tsx -> src/commands/command.ts", "src/env.ts -> src/lib/headless-mode.ts", "src/lib/detection/agentic.ts -> src/lib/agent/agent-interface.ts", - "src/lib/errors/agent-map.ts -> src/lib/agent/signals.ts", + "src/shared/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", diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 03b79a595..aedc37a40 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -285,7 +285,7 @@ describe('CLI argument parsing', () => { // from a buildSession arg — so assert the URL the run would actually fetch. async function skillsBaseUrl(): Promise<{ actual: string; local: string }> { const { getSkillsBaseUrl, LOCAL_SKILLS_BASE_URL } = await import( - '../lib/constants' + '@shared/constants' ); return { actual: getSkillsBaseUrl(), local: LOCAL_SKILLS_BASE_URL }; } diff --git a/src/__tests__/wizard-abort.test.ts b/src/__tests__/wizard-abort.test.ts index 829157408..66111f8b6 100644 --- a/src/__tests__/wizard-abort.test.ts +++ b/src/__tests__/wizard-abort.test.ts @@ -7,7 +7,7 @@ import { runCleanups, } from '@utils/wizard-abort'; import { analytics } from '@utils/analytics'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { getUI } from '../ui'; vi.mock('@utils/analytics'); diff --git a/src/commands/basic-integration/ci-install.ts b/src/commands/basic-integration/ci-install.ts index ba04ed382..9c24bc880 100644 --- a/src/commands/basic-integration/ci-install.ts +++ b/src/commands/basic-integration/ci-install.ts @@ -5,8 +5,8 @@ import { API_KEY_HINT, runWizardCI, runWizardHeadless } from '@lib/runners'; import type { NonInteractiveMode } from '@lib/runners'; import { provisionNewAccount } from '@utils/provisioning'; import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; -import { ErrorCodes, type ErrorCode } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes, type ErrorCode } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; type Options = Arguments & { region?: string; diff --git a/src/commands/basic-integration/non-interactive.ts b/src/commands/basic-integration/non-interactive.ts index 02a47675c..30877ee93 100644 --- a/src/commands/basic-integration/non-interactive.ts +++ b/src/commands/basic-integration/non-interactive.ts @@ -1,6 +1,6 @@ import { getUI } from '@ui'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; /** Print the "needs a TTY" error and exit. Used when no `--ci` flag and no TTY. */ export function failNonInteractive(): void { diff --git a/src/commands/basic-integration/playground.ts b/src/commands/basic-integration/playground.ts index 61eaaf0fc..92c5eab2a 100644 --- a/src/commands/basic-integration/playground.ts +++ b/src/commands/basic-integration/playground.ts @@ -1,4 +1,4 @@ -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import { startPlayground } from '@ui/tui/playground/start-playground'; /** Launch the TUI primitives playground. */ diff --git a/src/commands/basic-integration/skill.ts b/src/commands/basic-integration/skill.ts index a4a2bc088..5b41a205d 100644 --- a/src/commands/basic-integration/skill.ts +++ b/src/commands/basic-integration/skill.ts @@ -1,5 +1,5 @@ import type { Arguments } from 'yargs'; -import { POSTHOG_DOCS_URL } from '@lib/constants'; +import { POSTHOG_DOCS_URL } from '@shared/constants'; import { runWizard, runWizardCI } from '@lib/runners'; import { createSkillProgram } from '@lib/programs/agent-skill/index'; diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 4884df234..3b9385b97 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -1,8 +1,8 @@ import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { readApiKeyFromEnv } from '@utils/env-api-key'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; import { runWizard } from '@lib/runners'; import { posthogDoctorConfig, @@ -87,7 +87,7 @@ async function runDoctorCI(options: Record): Promise { } process.exit(1); } catch (error) { - const { ApiError } = await import('@lib/api'); + const { ApiError } = await import('@shared/api'); const message = error instanceof ApiError && error.statusCode === 401 ? 'Your PostHog API key is invalid or expired.' diff --git a/src/commands/factories/family-command-factory.ts b/src/commands/factories/family-command-factory.ts index 3fea1c7ea..23992e5f1 100644 --- a/src/commands/factories/family-command-factory.ts +++ b/src/commands/factories/family-command-factory.ts @@ -6,7 +6,7 @@ import { dispatchFamily, pickerChildrenToShow, } from '@lib/programs/dispatch-family'; -import { getSkillsBaseUrl } from '@lib/constants'; +import { getSkillsBaseUrl } from '@shared/constants'; import { fetchSkillMenu } from '@lib/wizard-tools'; import type { Command } from '../command'; diff --git a/src/commands/factories/shared.ts b/src/commands/factories/shared.ts index 0cda49719..cf2e73951 100644 --- a/src/commands/factories/shared.ts +++ b/src/commands/factories/shared.ts @@ -5,8 +5,8 @@ import { isHeadless } from '@lib/headless-mode'; import type { ProgramConfig } from '@lib/programs/program-step'; import { skillProgramOptions } from '../skill-program-options'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; /** * Dispatch a parsed yargs invocation to the wizard runner. Applies the diff --git a/src/commands/mcp/add.ts b/src/commands/mcp/add.ts index f1bf00116..f9b22e7a0 100644 --- a/src/commands/mcp/add.ts +++ b/src/commands/mcp/add.ts @@ -3,7 +3,7 @@ import { setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { headlessOption, isHeadless } from '@lib/headless-mode'; import { Program } from '@lib/programs/program-registry'; -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import type { Command } from '../command'; import { isTUIUnavailable } from './tui-availability'; diff --git a/src/commands/mcp/remove.ts b/src/commands/mcp/remove.ts index 7769056b2..26e34291e 100644 --- a/src/commands/mcp/remove.ts +++ b/src/commands/mcp/remove.ts @@ -3,7 +3,7 @@ import { setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { headlessOption, isHeadless } from '@lib/headless-mode'; import { Program } from '@lib/programs/program-registry'; -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import type { Command } from '../command'; import { isTUIUnavailable } from './tui-availability'; diff --git a/src/commands/mcp/tutorial.ts b/src/commands/mcp/tutorial.ts index 66e2443fe..eb6c0b495 100644 --- a/src/commands/mcp/tutorial.ts +++ b/src/commands/mcp/tutorial.ts @@ -2,9 +2,9 @@ import type { Arguments } from 'yargs'; import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { Program } from '@lib/programs/program-registry'; -import { VERSION } from '@lib/version'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { VERSION } from '@shared/version'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; import type { Command } from '../command'; export const mcpTutorialCommand: Command = { diff --git a/src/commands/skill.ts b/src/commands/skill.ts index 102eff5f8..01c21cc8b 100644 --- a/src/commands/skill.ts +++ b/src/commands/skill.ts @@ -1,14 +1,14 @@ import type { Arguments } from 'yargs'; -import { getSkillsBaseUrl } from '@lib/constants'; +import { getSkillsBaseUrl } from '@shared/constants'; import { fetchSkillMenu, type CliEntry } from '@lib/wizard-tools'; import { analytics } from '@utils/analytics'; import { runSkillMode } from './basic-integration/skill'; import { skillProgramOptions } from './skill-program-options'; import { runCommandHandler } from './factories/shared'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; import type { Command } from './command'; /** Read the `` positional (yargs camelCases the hyphenated key). */ diff --git a/src/commands/slack.ts b/src/commands/slack.ts index 4f0bf7743..cd857e6c2 100644 --- a/src/commands/slack.ts +++ b/src/commands/slack.ts @@ -2,9 +2,9 @@ import type { Arguments } from 'yargs'; import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { Program } from '@lib/programs/program-registry'; -import { VERSION } from '@lib/version'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { VERSION } from '@shared/version'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; import type { Command } from './command'; export const slackCommand: Command = { diff --git a/src/frameworks/android/android-wizard-agent.ts b/src/frameworks/android/android-wizard-agent.ts index dd9acaebf..789cf3746 100644 --- a/src/frameworks/android/android-wizard-agent.ts +++ b/src/frameworks/android/android-wizard-agent.ts @@ -1,7 +1,7 @@ /* Android (Kotlin) wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import * as fs from 'node:fs'; import * as path from 'node:path'; diff --git a/src/frameworks/angular/angular-wizard-agent.ts b/src/frameworks/angular/angular-wizard-agent.ts index 7db04b37a..bd1fdc675 100644 --- a/src/frameworks/angular/angular-wizard-agent.ts +++ b/src/frameworks/angular/angular-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/astro/astro-wizard-agent.ts b/src/frameworks/astro/astro-wizard-agent.ts index f427935ab..f8052cc30 100644 --- a/src/frameworks/astro/astro-wizard-agent.ts +++ b/src/frameworks/astro/astro-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/django/django-wizard-agent.ts b/src/frameworks/django/django-wizard-agent.ts index 30e2660aa..6abe33c2a 100644 --- a/src/frameworks/django/django-wizard-agent.ts +++ b/src/frameworks/django/django-wizard-agent.ts @@ -3,7 +3,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; import { detectPythonPackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; import { diff --git a/src/frameworks/elixir/elixir-wizard-agent.ts b/src/frameworks/elixir/elixir-wizard-agent.ts index 26eb007e1..f74b4e38e 100644 --- a/src/frameworks/elixir/elixir-wizard-agent.ts +++ b/src/frameworks/elixir/elixir-wizard-agent.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { mixPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; type ElixirContext = { phoenix?: boolean; diff --git a/src/frameworks/fastapi/fastapi-wizard-agent.ts b/src/frameworks/fastapi/fastapi-wizard-agent.ts index 61f0d8d8d..1a6763dbd 100644 --- a/src/frameworks/fastapi/fastapi-wizard-agent.ts +++ b/src/frameworks/fastapi/fastapi-wizard-agent.ts @@ -3,7 +3,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; import { detectPythonPackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getFastAPIVersion, getFastAPIProjectType, diff --git a/src/frameworks/flask/flask-wizard-agent.ts b/src/frameworks/flask/flask-wizard-agent.ts index 539afa840..8d143fda6 100644 --- a/src/frameworks/flask/flask-wizard-agent.ts +++ b/src/frameworks/flask/flask-wizard-agent.ts @@ -3,7 +3,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; import { detectPythonPackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; import { diff --git a/src/frameworks/flutter/flutter-wizard-agent.ts b/src/frameworks/flutter/flutter-wizard-agent.ts index 8a44cddfc..c2fb22371 100644 --- a/src/frameworks/flutter/flutter-wizard-agent.ts +++ b/src/frameworks/flutter/flutter-wizard-agent.ts @@ -1,7 +1,7 @@ /* Flutter wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { pubPackageManager } from '@lib/detection/package-manager'; diff --git a/src/frameworks/go/go-wizard-agent.ts b/src/frameworks/go/go-wizard-agent.ts index 6010e945b..f9e630506 100644 --- a/src/frameworks/go/go-wizard-agent.ts +++ b/src/frameworks/go/go-wizard-agent.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { goModulesPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; type GoContext = { goVersion?: string; diff --git a/src/frameworks/java/java-wizard-agent.ts b/src/frameworks/java/java-wizard-agent.ts index 9223a0525..1c9e26828 100644 --- a/src/frameworks/java/java-wizard-agent.ts +++ b/src/frameworks/java/java-wizard-agent.ts @@ -5,7 +5,7 @@ import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectJavaPackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; type JavaContext = { buildTool?: 'maven' | 'gradle'; diff --git a/src/frameworks/javascript-node/javascript-node-wizard-agent.ts b/src/frameworks/javascript-node/javascript-node-wizard-agent.ts index fb12fb38e..62519d721 100644 --- a/src/frameworks/javascript-node/javascript-node-wizard-agent.ts +++ b/src/frameworks/javascript-node/javascript-node-wizard-agent.ts @@ -1,6 +1,6 @@ /* Generic Node.js language wizard using posthog-agent with PostHog MCP */ import type { FrameworkConfig } from '@lib/framework-config'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { tryGetPackageJson } from '@utils/setup-utils'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; diff --git a/src/frameworks/javascript-web/javascript-web-wizard-agent.ts b/src/frameworks/javascript-web/javascript-web-wizard-agent.ts index 1f963eab3..cc3899c06 100644 --- a/src/frameworks/javascript-web/javascript-web-wizard-agent.ts +++ b/src/frameworks/javascript-web/javascript-web-wizard-agent.ts @@ -1,7 +1,7 @@ /* Generic JavaScript Web (client-side) wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { hasDeclaredDependency } from '@utils/package-json'; diff --git a/src/frameworks/kmp/kmp-wizard-agent.ts b/src/frameworks/kmp/kmp-wizard-agent.ts index 6fa6085d2..be2a3e969 100644 --- a/src/frameworks/kmp/kmp-wizard-agent.ts +++ b/src/frameworks/kmp/kmp-wizard-agent.ts @@ -1,6 +1,6 @@ /* Kotlin Multiplatform (KMP) wizard using posthog-agent with PostHog MCP */ import type { FrameworkConfig } from '../../lib/framework-config'; -import { Integration } from '../../lib/constants'; +import { Integration } from '@shared/constants'; import { gradlePackageManager } from '../../lib/detection/package-manager'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; diff --git a/src/frameworks/laravel/laravel-wizard-agent.ts b/src/frameworks/laravel/laravel-wizard-agent.ts index 36b81b971..b7c0c1651 100644 --- a/src/frameworks/laravel/laravel-wizard-agent.ts +++ b/src/frameworks/laravel/laravel-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { composerPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import * as fs from 'node:fs'; import * as path from 'node:path'; diff --git a/src/frameworks/nextjs/nextjs-wizard-agent.ts b/src/frameworks/nextjs/nextjs-wizard-agent.ts index d98442f5a..614d9e7aa 100644 --- a/src/frameworks/nextjs/nextjs-wizard-agent.ts +++ b/src/frameworks/nextjs/nextjs-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/nuxt/nuxt-wizard-agent.ts b/src/frameworks/nuxt/nuxt-wizard-agent.ts index 56899b903..94edb5173 100644 --- a/src/frameworks/nuxt/nuxt-wizard-agent.ts +++ b/src/frameworks/nuxt/nuxt-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/python/python-wizard-agent.ts b/src/frameworks/python/python-wizard-agent.ts index fc44b370b..15d88cd69 100644 --- a/src/frameworks/python/python-wizard-agent.ts +++ b/src/frameworks/python/python-wizard-agent.ts @@ -3,7 +3,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; import { detectPythonPackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import fg from 'fast-glob'; import * as fs from 'node:fs'; import * as path from 'node:path'; diff --git a/src/frameworks/rails/rails-wizard-agent.ts b/src/frameworks/rails/rails-wizard-agent.ts index f473ac98b..e2df48c59 100644 --- a/src/frameworks/rails/rails-wizard-agent.ts +++ b/src/frameworks/rails/rails-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { bundlerPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getRailsVersion, getRailsProjectType, diff --git a/src/frameworks/react-native/react-native-wizard-agent.ts b/src/frameworks/react-native/react-native-wizard-agent.ts index 7d231781c..c4a489c71 100644 --- a/src/frameworks/react-native/react-native-wizard-agent.ts +++ b/src/frameworks/react-native/react-native-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/react-router/react-router-wizard-agent.ts b/src/frameworks/react-router/react-router-wizard-agent.ts index e1c211ebe..4c19a4ac4 100644 --- a/src/frameworks/react-router/react-router-wizard-agent.ts +++ b/src/frameworks/react-router/react-router-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/ruby/ruby-wizard-agent.ts b/src/frameworks/ruby/ruby-wizard-agent.ts index aa63873b9..508ab33a5 100644 --- a/src/frameworks/ruby/ruby-wizard-agent.ts +++ b/src/frameworks/ruby/ruby-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { bundlerPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getRubyVersion, getRubyVersionBucket, diff --git a/src/frameworks/rust/rust-wizard-agent.ts b/src/frameworks/rust/rust-wizard-agent.ts index 6c0aef1e2..3113a6708 100644 --- a/src/frameworks/rust/rust-wizard-agent.ts +++ b/src/frameworks/rust/rust-wizard-agent.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { cargoPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; type RustContext = { asyncRuntime?: boolean; diff --git a/src/frameworks/svelte/svelte-wizard-agent.ts b/src/frameworks/svelte/svelte-wizard-agent.ts index e84e2cef7..5d7777fdc 100644 --- a/src/frameworks/svelte/svelte-wizard-agent.ts +++ b/src/frameworks/svelte/svelte-wizard-agent.ts @@ -1,7 +1,7 @@ /* SvelteKit wizard using posthog-agent with PostHog MCP */ import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, hasDeclaredDependency, diff --git a/src/frameworks/swift/swift-wizard-agent.ts b/src/frameworks/swift/swift-wizard-agent.ts index 5e6343b87..049255b64 100644 --- a/src/frameworks/swift/swift-wizard-agent.ts +++ b/src/frameworks/swift/swift-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { swiftPackageManager } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import fg from 'fast-glob'; import * as fs from 'node:fs'; diff --git a/src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts b/src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts index c63245a85..07e43d0ad 100644 --- a/src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts +++ b/src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts b/src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts index a279ef6bb..f28eaa4a9 100644 --- a/src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts +++ b/src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts @@ -2,7 +2,7 @@ import type { WizardRunOptions } from '@utils/types'; import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/frameworks/vue/vue-wizard-agent.ts b/src/frameworks/vue/vue-wizard-agent.ts index a37492a8d..dd61b3ab4 100644 --- a/src/frameworks/vue/vue-wizard-agent.ts +++ b/src/frameworks/vue/vue-wizard-agent.ts @@ -1,7 +1,7 @@ /* Vue wizard using posthog-agent with PostHog MCP */ import type { FrameworkConfig } from '@lib/framework-config'; import { detectNodePackageManagers } from '@lib/detection/package-manager'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getDeclaredVersion, getInstalledPackageVersion, diff --git a/src/lib/__tests__/agent-interface.test.ts b/src/lib/__tests__/agent-interface.test.ts index 01f6fe3c9..8c40c8618 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/lib/__tests__/agent-interface.test.ts @@ -12,7 +12,7 @@ import { import { AgentOutputSignals } from '@lib/agent/output-signals'; import { RESUME_INSTRUCTION } from '@lib/agent/signals'; import { analytics } from '@utils/analytics'; -import { Sequence } from '@lib/constants'; +import { Sequence } from '@shared/constants'; import type { WizardRunOptions } from '@utils/types'; import type { SpinnerHandle } from '@ui'; import { diff --git a/src/lib/__tests__/gateway-session.test.ts b/src/lib/__tests__/gateway-session.test.ts index ce0966fc5..315ec407d 100644 --- a/src/lib/__tests__/gateway-session.test.ts +++ b/src/lib/__tests__/gateway-session.test.ts @@ -10,15 +10,15 @@ import { isTrustedGatewayUrl, resetGatewaySession, } from '@lib/gateway-session'; -import type { HostResolution } from '@lib/host-resolution'; -import { ErrorCodes } from '@lib/errors'; +import type { HostResolution } from '@shared/host-resolution'; +import { ErrorCodes } from '@shared/errors'; import { WizardError } from '@utils/wizard-abort'; import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; -import { checkLlmGatewayHealth } from '@lib/health-checks/endpoints'; -import { ServiceHealthStatus } from '@lib/health-checks/types'; +import { checkLlmGatewayHealth } from '@shared/health-checks/endpoints'; +import { ServiceHealthStatus } from '@shared/health-checks/types'; -vi.mock('@lib/health-checks/endpoints', () => ({ +vi.mock('@shared/health-checks/endpoints', () => ({ checkLlmGatewayHealth: vi.fn(), })); diff --git a/src/lib/__tests__/mcp-role-prompts.test.ts b/src/lib/__tests__/mcp-role-prompts.test.ts index 704f44cf7..be84e1c69 100644 --- a/src/lib/__tests__/mcp-role-prompts.test.ts +++ b/src/lib/__tests__/mcp-role-prompts.test.ts @@ -11,7 +11,7 @@ import { FOLLOW_UP_EXIT_SENTINEL, TAILORED_ROLES, } from '@lib/mcp-role-prompts'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { degradedProfile, type EventVolume, diff --git a/src/lib/__tests__/wizard-spellbook.test.ts b/src/lib/__tests__/wizard-spellbook.test.ts index 727b5f5f1..02f017b0a 100644 --- a/src/lib/__tests__/wizard-spellbook.test.ts +++ b/src/lib/__tests__/wizard-spellbook.test.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises'; import os from 'os'; import path from 'path'; -import { Integration } from '../constants'; +import { Integration } from '@shared/constants'; import type { ProgramConfig } from '../programs/program-step'; import { buildSession } from '../wizard-session'; import { writeWizardSpellbook } from '../wizard-spellbook'; diff --git a/src/lib/__tests__/yara-hooks.test.ts b/src/lib/__tests__/yara-hooks.test.ts index 239dbc5e3..8f3e8a338 100644 --- a/src/lib/__tests__/yara-hooks.test.ts +++ b/src/lib/__tests__/yara-hooks.test.ts @@ -21,7 +21,7 @@ vi.mock('fs'); vi.mock('fast-glob'); // Mock isSkillInstallCommand from skill-install (extracted to break circular dep) -vi.mock('../skill-install', () => ({ +vi.mock('@shared/skill-install', () => ({ isSkillInstallCommand: (command: string) => command.startsWith('mkdir -p .claude/skills/') && command.includes('curl -sL') && diff --git a/src/lib/agent/__tests__/agent-prompt-loader.test.ts b/src/lib/agent/__tests__/agent-prompt-loader.test.ts index ab23e6a49..b04c7a925 100644 --- a/src/lib/agent/__tests__/agent-prompt-loader.test.ts +++ b/src/lib/agent/__tests__/agent-prompt-loader.test.ts @@ -18,7 +18,7 @@ import { type OrchestratorPromptContext, } from '../agent-prompt-loader'; import { QueueStore } from '@lib/agent/runner/sequence/orchestrator/queue'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; function tmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'agent-loader-test-')); diff --git a/src/lib/agent/__tests__/agent-prompt.test.ts b/src/lib/agent/__tests__/agent-prompt.test.ts index e6bad880c..302525c83 100644 --- a/src/lib/agent/__tests__/agent-prompt.test.ts +++ b/src/lib/agent/__tests__/agent-prompt.test.ts @@ -1,6 +1,6 @@ import { assemblePrompt, type PromptContext } from '@lib/agent/agent-prompt'; import type { AgentRunDefinition } from '@lib/agent/runner'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; function makeRunDef( overrides: Partial = {}, diff --git a/src/lib/agent/__tests__/commandments.test.ts b/src/lib/agent/__tests__/commandments.test.ts index b00a74a4f..1a0dc6ffe 100644 --- a/src/lib/agent/__tests__/commandments.test.ts +++ b/src/lib/agent/__tests__/commandments.test.ts @@ -1,6 +1,6 @@ import { WIZARD_COMMANDMENTS } from '@lib/agent/commandments'; import { assembleCommandments } from '@lib/agent/runner/switchboard/commandments'; -import { Harness, Sequence } from '@lib/constants'; +import { Harness, Sequence } from '@shared/constants'; const global = WIZARD_COMMANDMENTS.join('\n'); diff --git a/src/lib/agent/__tests__/progress-collector.test.ts b/src/lib/agent/__tests__/progress-collector.test.ts index 1d6e0740f..492d5f1b7 100644 --- a/src/lib/agent/__tests__/progress-collector.test.ts +++ b/src/lib/agent/__tests__/progress-collector.test.ts @@ -1,6 +1,6 @@ import { OutroKind } from '@lib/wizard-session'; import { createProgressCollector } from '../runner/shared/progress-collector'; -import { MAX_STATUS_MESSAGES } from '@lib/status-history'; +import { MAX_STATUS_MESSAGES } from '@shared/status-history'; it('retains a bounded FIFO of statuses and drops consecutive duplicates', () => { const observer = vi.fn(); diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts index c419a2e48..b8994dc93 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -9,14 +9,14 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { Harness, Sequence, DEFAULT_AGENT_MODEL } from '@lib/constants'; -import { HostResolution } from '@lib/host-resolution'; +import { Harness, Sequence, DEFAULT_AGENT_MODEL } from '@shared/constants'; +import { HostResolution } from '@shared/host-resolution'; import { OutroKind, type AskAnswers, type PendingQuestion, } from '@lib/wizard-session'; -import { AGENT_ERROR_CODE, ErrorCodes } from '@lib/errors'; +import { AGENT_ERROR_CODE, ErrorCodes } from '@shared/errors'; import { AgentErrorType } from '@lib/agent/signals'; import type { AgentFailure } from '@lib/agent/runner/shared/types'; import type { AgentProgress } from '@lib/agent/progress'; diff --git a/src/lib/agent/__tests__/run-tags.test.ts b/src/lib/agent/__tests__/run-tags.test.ts index b255653f7..8d88772ee 100644 --- a/src/lib/agent/__tests__/run-tags.test.ts +++ b/src/lib/agent/__tests__/run-tags.test.ts @@ -1,5 +1,5 @@ import { buildRunTags } from '@lib/agent/agent-interface'; -import { CallType } from '@lib/constants'; +import { CallType } from '@shared/constants'; describe('buildRunTags', () => { it('carries the run identifiers as gateway trace tags', () => { diff --git a/src/lib/agent/__tests__/triage-provider.test.ts b/src/lib/agent/__tests__/triage-provider.test.ts index d51d82b9e..f533c146e 100644 --- a/src/lib/agent/__tests__/triage-provider.test.ts +++ b/src/lib/agent/__tests__/triage-provider.test.ts @@ -5,7 +5,7 @@ import { GPT5_6_LUNA_MODEL, HAIKU_TRIAGE_MODEL, Harness, -} from '@lib/constants'; +} from '@shared/constants'; import { triageModelFor } from '@lib/agent/runner/switchboard/models'; vi.mock('@earendil-works/pi-ai', () => ({ completeSimple: vi.fn() })); diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index 2b47993ca..fa3fa8904 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -27,10 +27,10 @@ import { AWS_SKILLS_BASE_URL, type AdditionalFeature, ADDITIONAL_FEATURE_PROMPTS, -} from '@lib/constants'; +} from '@shared/constants'; import type { AgentFailure } from './runner/shared/types'; import { createCustomHeaders } from '@utils/custom-headers'; -import type { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@shared/host-resolution'; import { buildWizardPropertiesBlob, gatewayAuth, @@ -55,8 +55,8 @@ import { REMARK_INSTRUCTION, RESUME_INSTRUCTION, } from './signals'; -import { classifyAuthFailure, WizardError } from '@lib/errors'; -import { isGrantRevoked } from '@lib/auth-session-state'; +import { classifyAuthFailure, WizardError } from '@shared/errors'; +import { isGrantRevoked } from '@shared/auth-session-state'; import { AgentOutputSignals } from './output-signals'; // Signal vocabulary and the output parser live in dedicated modules; re-export @@ -421,7 +421,7 @@ export function buildAgentEnv( } // Re-export for backwards compatibility — canonical source is skill-install.ts -export { isSkillInstallCommand } from '@lib/skill-install'; +export { isSkillInstallCommand } from '@shared/skill-install'; /** * Permission hook that allows only safe commands. Bash commands are gated by diff --git a/src/lib/agent/agent-prompt-loader.ts b/src/lib/agent/agent-prompt-loader.ts index c69d8fc87..e4372658f 100644 --- a/src/lib/agent/agent-prompt-loader.ts +++ b/src/lib/agent/agent-prompt-loader.ts @@ -20,14 +20,14 @@ import type { QueuedTask, } from './runner/sequence/orchestrator/queue'; import type { ResolvedTask } from './runner/sequence/orchestrator/executor'; -import type { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@shared/host-resolution'; import { isThinkingLevel, type ThinkingLevel, } from './runner/switchboard/models'; import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; -import { fetchWithRetry } from '@lib/fetch-retry'; +import { fetchWithRetry } from '@shared/fetch-retry'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools/tools'; /** diff --git a/src/lib/agent/agent-prompt.ts b/src/lib/agent/agent-prompt.ts index 4987adcfb..85abfe958 100644 --- a/src/lib/agent/agent-prompt.ts +++ b/src/lib/agent/agent-prompt.ts @@ -8,7 +8,7 @@ */ import type { AgentRunDefinition } from './runner/shared/types'; -import type { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@shared/host-resolution'; /** * Values available to prompt builders after OAuth completes. diff --git a/src/lib/agent/aio-capture.ts b/src/lib/agent/aio-capture.ts index ad37d252f..be4182c3c 100644 --- a/src/lib/agent/aio-capture.ts +++ b/src/lib/agent/aio-capture.ts @@ -25,7 +25,7 @@ */ import { logToFile } from '@utils/debug'; -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; /** Conversation turn shape both harnesses normalize to. Matches Anthropic * message role/content conventions, which is also what AIO's Conversation diff --git a/src/lib/agent/mcp-prompt-streaming.ts b/src/lib/agent/mcp-prompt-streaming.ts index f3d0354ec..bf4b2414f 100644 --- a/src/lib/agent/mcp-prompt-streaming.ts +++ b/src/lib/agent/mcp-prompt-streaming.ts @@ -12,8 +12,8 @@ * `for await (...)` and render as they arrive. */ -import type { Credentials } from '@lib/api'; -import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@lib/constants'; +import type { Credentials } from '@shared/api'; +import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@shared/constants'; import { logToFile } from '@utils/debug'; import { gatewayAuth } from '@lib/gateway-session'; import { buildAgentEnv, buildRunTags } from '@lib/agent/agent-interface'; diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index 3bc45c79e..780c9f039 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -46,7 +46,7 @@ export interface OutroData { /** Report file the agent wrote (e.g. "posthog-setup-report.md") */ reportFile?: string; /** Stable machine-readable error code from the error catalog (@lib/errors). */ - errorCode?: import('@lib/errors').ErrorCode; + errorCode?: import('@shared/errors').ErrorCode; /** Structured context for the error code; safe for telemetry payloads. */ errorDetail?: Record; /** PostHog dashboard URL the program created on the user's behalf. */ diff --git a/src/lib/agent/runner/__tests__/switchboard.test.ts b/src/lib/agent/runner/__tests__/switchboard.test.ts index 5386fc312..3ae1c283a 100644 --- a/src/lib/agent/runner/__tests__/switchboard.test.ts +++ b/src/lib/agent/runner/__tests__/switchboard.test.ts @@ -21,7 +21,7 @@ import { Harness, Sequence, WIZARD_ORCHESTRATOR_FLAG_KEY, -} from '@lib/constants'; +} from '@shared/constants'; import { PROGRAM_BINDINGS, DEFAULT_BINDING, diff --git a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts b/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts index 4102211ea..aa4ed9038 100644 --- a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts +++ b/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts @@ -3,8 +3,8 @@ import { createAskBridge } from '../../../shared/ask'; import { anthropicBackend } from '..'; import type { BackendRunInputs, TaskRunInputs } from '../../types'; import type { AskAnswers } from '@lib/wizard-session'; -import { Harness, Sequence } from '@lib/constants'; -import { HostResolution } from '@lib/host-resolution'; +import { Harness, Sequence } from '@shared/constants'; +import { HostResolution } from '@shared/host-resolution'; vi.mock('@utils/analytics'); vi.mock('@utils/debug'); diff --git a/src/lib/agent/runner/harness/anthropic/index.ts b/src/lib/agent/runner/harness/anthropic/index.ts index 1b967a662..3460b3e27 100644 --- a/src/lib/agent/runner/harness/anthropic/index.ts +++ b/src/lib/agent/runner/harness/anthropic/index.ts @@ -1,6 +1,6 @@ // Supported legacy SDK fallback; both this adapter and Pi implement run and runTask. -import { Harness } from '@lib/constants'; +import { Harness } from '@shared/constants'; import { initializeAgent, runAgent as executeAgent, diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index b14df2a37..0b677347c 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -20,7 +20,7 @@ import { Sequence, WIZARD_REMARK_EVENT_NAME, WIZARD_USER_AGENT, -} from '@lib/constants'; +} from '@shared/constants'; import { analytics } from '@utils/analytics'; import { AgentErrorType } from '@lib/agent/agent-interface'; import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent/signals'; diff --git a/src/lib/agent/runner/harness/pi/mcp.ts b/src/lib/agent/runner/harness/pi/mcp.ts index eeaa3607a..8f553a983 100644 --- a/src/lib/agent/runner/harness/pi/mcp.ts +++ b/src/lib/agent/runner/harness/pi/mcp.ts @@ -7,7 +7,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { createJiti } from 'jiti'; -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import { logToFile } from '@utils/debug'; const MCP_TOKEN_ENV = 'POSTHOG_MCP_TOKEN'; diff --git a/src/lib/agent/runner/harness/pi/runtime-notes.ts b/src/lib/agent/runner/harness/pi/runtime-notes.ts index 0d9dabfa3..e74c9c2a7 100644 --- a/src/lib/agent/runner/harness/pi/runtime-notes.ts +++ b/src/lib/agent/runner/harness/pi/runtime-notes.ts @@ -12,7 +12,7 @@ * resolved sequence calls for — shared notes always, mode notes by sequence, and * capability notes only when the task actually holds that tool. */ -import { Sequence } from '@lib/constants'; +import { Sequence } from '@shared/constants'; const HEADER = [ '## This runtime', diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index af9017a21..90f08530a 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -23,7 +23,7 @@ import { Sequence, WIZARD_REMARK_EVENT_NAME, WIZARD_USER_AGENT, -} from '@lib/constants'; +} from '@shared/constants'; import { allowsPostHogMcp, queueTools, diff --git a/src/lib/agent/runner/harness/pi/tools.ts b/src/lib/agent/runner/harness/pi/tools.ts index 117d7c349..7b1b66f44 100644 --- a/src/lib/agent/runner/harness/pi/tools.ts +++ b/src/lib/agent/runner/harness/pi/tools.ts @@ -62,12 +62,12 @@ import { PUBLISH_HANDOFF_TOOL_NAME, publishHandoff, } from '@lib/wizard-tools/handoff'; -import { createSecretVault } from '@lib/secret-vault'; +import { createSecretVault } from '@shared/secret-vault'; import { AUDIT_CHECKS_FILE, type AuditCheck, type AuditStatus, -} from '@lib/audit-ledger'; +} from '@shared/audit-ledger'; import { makeMutex } from '@utils/atomic-ledger'; import { withMode } from './index'; import { diff --git a/src/lib/agent/runner/harness/types.ts b/src/lib/agent/runner/harness/types.ts index cff145347..b4a2d64eb 100644 --- a/src/lib/agent/runner/harness/types.ts +++ b/src/lib/agent/runner/harness/types.ts @@ -19,8 +19,8 @@ * the run's result. */ -import type { AdditionalFeature } from '@lib/constants'; -import type { Harness } from '@lib/constants'; +import type { AdditionalFeature } from '@shared/constants'; +import type { Harness } from '@shared/constants'; import type { WizardAskBridge } from '@lib/wizard-ask-bridge'; import type { AgentErrorType } from '@lib/agent/agent-interface'; import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index 979a19723..67e06c50f 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -22,10 +22,10 @@ * behavior on top of this call for every existing caller. */ -import { Sequence } from '@lib/constants'; +import { Sequence } from '@shared/constants'; import { RunOutcome } from './shared/types'; export { RunOutcome } from './shared/types'; -import { classifyRunFailure } from '@lib/errors'; +import { classifyRunFailure } from '@shared/errors'; import { logToFile } from '@utils/debug'; import type { RunAgentOptions, diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index d316989ca..afdf90084 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -14,7 +14,7 @@ import { OutroKind, type OutroData } from '@lib/agent/progress'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; import { logToFile } from '@utils/debug'; import { createBenchmarkPipeline } from '../../../middleware/benchmark'; -import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@lib/errors'; +import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@shared/errors'; import { analytics } from '@utils/analytics'; import { formatYaraAbortMessage } from '../../../yara-hooks'; import { installSkillById } from '../../../wizard-tools'; diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts index b3f302b37..a8e5483ad 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { resolveSkillVariantId } from '../orchestrator-runner'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { expandBundleEntry } from '@lib/wizard-tools'; import type { SkillEntry } from '@lib/wizard-tools'; diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 43a3666a7..c68917390 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -23,7 +23,7 @@ import { } from 'fs'; import * as path from 'path'; import { OutroKind, type TaskNotice } from '@lib/agent/progress'; -import { POSTHOG_DOCS_URL, WIZARD_CONTACT_EMAIL } from '@lib/constants'; +import { POSTHOG_DOCS_URL, WIZARD_CONTACT_EMAIL } from '@shared/constants'; import { installSkillById, fetchSkillMenu, @@ -33,7 +33,7 @@ import { analytics } from '@utils/analytics'; import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; -import { ErrorCodes, WizardError } from '@lib/errors'; +import { ErrorCodes, WizardError } from '@shared/errors'; import type { AgentInteraction } from '@lib/agent/progress'; import type { AgentFailure, diff --git a/src/lib/agent/runner/shared/bootstrap.ts b/src/lib/agent/runner/shared/bootstrap.ts index 7384c5f1a..d666daee5 100644 --- a/src/lib/agent/runner/shared/bootstrap.ts +++ b/src/lib/agent/runner/shared/bootstrap.ts @@ -11,9 +11,9 @@ import { createTriageLLMProvider } from '@lib/agent/triage-provider'; import { gatewayAuth } from '@lib/gateway-session'; import { logToFile } from '@utils/debug'; -import { CallType, IS_DEV } from '@lib/constants'; -import { VERSION } from '@lib/version'; -import { mcpUrlFor } from '@lib/host-resolution'; +import { CallType, IS_DEV } from '@shared/constants'; +import { VERSION } from '@shared/version'; +import { mcpUrlFor } from '@shared/host-resolution'; import type { WizardRunOptions } from '@utils/types'; import type { BootstrapResult, RunConfig, RunFlags, RunInput } from './types'; diff --git a/src/lib/agent/runner/shared/errors.ts b/src/lib/agent/runner/shared/errors.ts index d991e4242..c5b6c27f9 100644 --- a/src/lib/agent/runner/shared/errors.ts +++ b/src/lib/agent/runner/shared/errors.ts @@ -3,8 +3,8 @@ */ import type { InstallSkillResult } from '@lib/wizard-tools'; -import { skillErrorCode } from '@lib/errors'; -import { WizardError } from '@lib/errors'; +import { skillErrorCode } from '@shared/errors'; +import { WizardError } from '@shared/errors'; import { RunOutcome, type AgentFailure, type SequenceResult } from './types'; export const failed = (failure: AgentFailure): SequenceResult => ({ diff --git a/src/lib/agent/runner/shared/progress-collector.ts b/src/lib/agent/runner/shared/progress-collector.ts index 42a9da5f8..8bc63e7da 100644 --- a/src/lib/agent/runner/shared/progress-collector.ts +++ b/src/lib/agent/runner/shared/progress-collector.ts @@ -9,7 +9,7 @@ */ import { logToFile } from '@utils/debug'; -import { appendStatus } from '@lib/status-history'; +import { appendStatus } from '@shared/status-history'; import type { AgentProgress, ProgressEmitter, diff --git a/src/lib/agent/runner/shared/types.ts b/src/lib/agent/runner/shared/types.ts index ea339af3e..e5c7cdfe8 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/lib/agent/runner/shared/types.ts @@ -9,15 +9,15 @@ * that rebuilds today's session-driven behavior on top of this contract. */ -import type { AdditionalFeature } from '@lib/constants'; +import type { AdditionalFeature } from '@shared/constants'; import type { CloudRegion } from '@utils/types'; -import type { Credentials } from '@lib/api'; +import type { Credentials } from '@shared/api'; import type { OutroData, TaskNotice } from '@lib/agent/progress'; import type { PromptContext } from '@lib/agent/agent-prompt'; import type { PackageManagerDetector } from '@utils/package-manager'; -import type { ApiProject, ApiUser } from '@lib/api'; -import type { Harness, Integration, Sequence } from '@lib/constants'; -import type { ErrorCode } from '@lib/errors'; +import type { ApiProject, ApiUser } from '@shared/api'; +import type { Harness, Integration, Sequence } from '@shared/constants'; +import type { ErrorCode } from '@shared/errors'; import type { LLMProvider } from '@posthog/warlock'; import type { AgentInteraction, ProgressEmitter } from '@lib/agent/progress'; import type { EffortLevel } from '../switchboard/models'; diff --git a/src/lib/agent/runner/switchboard/commandments.ts b/src/lib/agent/runner/switchboard/commandments.ts index 8cc9aed08..5a71c51e7 100644 --- a/src/lib/agent/runner/switchboard/commandments.ts +++ b/src/lib/agent/runner/switchboard/commandments.ts @@ -12,7 +12,7 @@ * an import cycle. */ -import { Harness, Sequence } from '@lib/constants'; +import { Harness, Sequence } from '@shared/constants'; import { WIZARD_COMMANDMENTS } from '@lib/agent/commandments'; import { piRuntimeNotes, type RuntimeCaps } from '../harness/pi/runtime-notes'; diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts b/src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts index ebddc32e2..e3adfa25d 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts +++ b/src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts @@ -4,7 +4,7 @@ * `runBindingCases` turns a table into `it` blocks — specs stay declarative. */ import { describe, it, expect } from 'vitest'; -import { GPT5_6_SOL_MODEL, Harness, Sequence } from '@lib/constants'; +import { GPT5_6_SOL_MODEL, Harness, Sequence } from '@shared/constants'; import { resolveBinding, type SwitchboardCtx, diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts index f0540ab5b..8d3360566 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { join, dirname } from 'node:path'; import { describe, it, expect, vi } from 'vitest'; import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; -import * as constants from '@lib/constants'; +import * as constants from '@shared/constants'; import { DEFAULT_AGENT_MODEL, GPT5_6_SOL_MODEL, @@ -14,7 +14,7 @@ import { WIZARD_ORCHESTRATOR_FLAG_KEY, WIZARD_ORCHESTRATOR_OVERRIDE_FLAG_KEY, WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, -} from '@lib/constants'; +} from '@shared/constants'; import { areSeededTasksEnabled, resolveBinding, diff --git a/src/lib/agent/runner/switchboard/flags/index.ts b/src/lib/agent/runner/switchboard/flags/index.ts index b9d3d03a2..5b376d3ef 100644 --- a/src/lib/agent/runner/switchboard/flags/index.ts +++ b/src/lib/agent/runner/switchboard/flags/index.ts @@ -6,7 +6,7 @@ */ import { RUN_SURFACE } from '@env'; import { logToFile } from '@utils/debug'; -import type { Sequence } from '@lib/constants'; +import type { Sequence } from '@shared/constants'; import type { ProgramId } from '@lib/programs/program-registry'; import { ORCHESTRATOR_HARNESS_ROUTE, diff --git a/src/lib/agent/runner/switchboard/flags/orchestrator.ts b/src/lib/agent/runner/switchboard/flags/orchestrator.ts index 544e00b83..3bc6f09ad 100644 --- a/src/lib/agent/runner/switchboard/flags/orchestrator.ts +++ b/src/lib/agent/runner/switchboard/flags/orchestrator.ts @@ -5,7 +5,7 @@ import { WIZARD_ORCHESTRATOR_FLAG_KEY, WIZARD_ORCHESTRATOR_OVERRIDE_FLAG_KEY, WIZARD_ORCHESTRATOR_SEEDED_TASKS_FLAG_KEY, -} from '@lib/constants'; +} from '@shared/constants'; import type { HarnessExperiment, SequenceExperiment } from './schemes'; export const ORCHESTRATOR_SEQUENCE_ROUTE: SequenceExperiment = { diff --git a/src/lib/agent/runner/switchboard/flags/schemes.ts b/src/lib/agent/runner/switchboard/flags/schemes.ts index f55f30141..e30f48c0e 100644 --- a/src/lib/agent/runner/switchboard/flags/schemes.ts +++ b/src/lib/agent/runner/switchboard/flags/schemes.ts @@ -12,7 +12,7 @@ import { Harness, Sequence, SONNET_5_MODEL, -} from '@lib/constants'; +} from '@shared/constants'; import type { ProgramId } from '@lib/programs/program-registry'; import { logToFile } from '@utils/debug'; import type { EffortLevel } from '../models'; diff --git a/src/lib/agent/runner/switchboard/flags/self-driving.ts b/src/lib/agent/runner/switchboard/flags/self-driving.ts index beb34ea90..2f1e8fcab 100644 --- a/src/lib/agent/runner/switchboard/flags/self-driving.ts +++ b/src/lib/agent/runner/switchboard/flags/self-driving.ts @@ -6,7 +6,7 @@ * Routes: ONLY `self-driving` — any axis the payload pins. A missing or * invalid payload keeps the non-flagged binding default. */ -import { WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY } from '@lib/constants'; +import { WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY } from '@shared/constants'; import type { HarnessExperiment } from './schemes'; export const SELF_DRIVING_EXPERIMENT: HarnessExperiment = { diff --git a/src/lib/agent/runner/switchboard/harness.ts b/src/lib/agent/runner/switchboard/harness.ts index 8710a166f..0f5673ed2 100644 --- a/src/lib/agent/runner/switchboard/harness.ts +++ b/src/lib/agent/runner/switchboard/harness.ts @@ -3,7 +3,7 @@ */ import { IS_PRODUCTION_BUILD } from '@env'; -import { Harness } from '@lib/constants'; +import { Harness } from '@shared/constants'; import { logToFile } from '@utils/debug'; import { anthropicBackend } from '../harness/anthropic'; import { piBackend } from '../harness/pi'; diff --git a/src/lib/agent/runner/switchboard/index.ts b/src/lib/agent/runner/switchboard/index.ts index f6d641a66..842176c16 100644 --- a/src/lib/agent/runner/switchboard/index.ts +++ b/src/lib/agent/runner/switchboard/index.ts @@ -6,7 +6,7 @@ import { GPT5_6_TERRA_MODEL, Harness, Sequence, -} from '@lib/constants'; +} from '@shared/constants'; import type { ProgramId } from '@lib/programs/program-registry'; import { resolveHarness } from './harness'; import type { EffortLevel } from './models'; diff --git a/src/lib/agent/runner/switchboard/models.ts b/src/lib/agent/runner/switchboard/models.ts index ab8d4b202..d72d37c21 100644 --- a/src/lib/agent/runner/switchboard/models.ts +++ b/src/lib/agent/runner/switchboard/models.ts @@ -7,7 +7,7 @@ import { GPT5_6_TERRA_MODEL, HAIKU_TRIAGE_MODEL, Harness, -} from '@lib/constants'; +} from '@shared/constants'; /** Reasoning effort, mapped by each harness to its provider transport. */ const THINKING_LEVELS = [ diff --git a/src/lib/agent/runner/switchboard/sequence.ts b/src/lib/agent/runner/switchboard/sequence.ts index 3d2730405..b6d241720 100644 --- a/src/lib/agent/runner/switchboard/sequence.ts +++ b/src/lib/agent/runner/switchboard/sequence.ts @@ -4,7 +4,7 @@ */ import { IS_PRODUCTION_BUILD } from '@env'; -import { Sequence } from '@lib/constants'; +import { Sequence } from '@shared/constants'; import { logToFile } from '@utils/debug'; import { isOrchestratorEnabled, diff --git a/src/lib/agent/token-pricing.ts b/src/lib/agent/token-pricing.ts index 192168207..74a4b84d9 100644 --- a/src/lib/agent/token-pricing.ts +++ b/src/lib/agent/token-pricing.ts @@ -35,7 +35,7 @@ import { DEFAULT_AGENT_MODEL, HAIKU_MODEL, SONNET_5_MODEL, -} from '@lib/constants'; +} from '@shared/constants'; export interface PricePerMtok { input: number; diff --git a/src/lib/agent/triage-provider.ts b/src/lib/agent/triage-provider.ts index dfb03861d..092bccc53 100644 --- a/src/lib/agent/triage-provider.ts +++ b/src/lib/agent/triage-provider.ts @@ -5,7 +5,7 @@ * transport, reasoning effort and trace headers come from one place. */ -import { Harness } from '@lib/constants'; +import { Harness } from '@shared/constants'; import { logToFile } from '@utils/debug'; import { buildGatewayModel, diff --git a/src/lib/detection/__tests__/agentic-progress.test.ts b/src/lib/detection/__tests__/agentic-progress.test.ts index 387bb24fc..1f9db65c4 100644 --- a/src/lib/detection/__tests__/agentic-progress.test.ts +++ b/src/lib/detection/__tests__/agentic-progress.test.ts @@ -4,7 +4,7 @@ import { runAgent as executeAgent, } from '@lib/agent/agent-interface'; import { buildSession } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import { getUI } from '@ui'; vi.mock('@utils/debug'); diff --git a/src/lib/detection/__tests__/framework.test.ts b/src/lib/detection/__tests__/framework.test.ts index 947159368..cd2a333f0 100644 --- a/src/lib/detection/__tests__/framework.test.ts +++ b/src/lib/detection/__tests__/framework.test.ts @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { detectFramework } from '@lib/detection/framework'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { ANDROID_AGENT_CONFIG } from '../../../frameworks/android/android-wizard-agent'; import { KMP_AGENT_CONFIG } from '../../../frameworks/kmp/kmp-wizard-agent'; import { ELIXIR_AGENT_CONFIG } from '../../../frameworks/elixir/elixir-wizard-agent'; diff --git a/src/lib/detection/__tests__/project-scope.test.ts b/src/lib/detection/__tests__/project-scope.test.ts index 18f9173dc..c33db784d 100644 --- a/src/lib/detection/__tests__/project-scope.test.ts +++ b/src/lib/detection/__tests__/project-scope.test.ts @@ -9,7 +9,7 @@ import { import { AGENTIC_DETECTION_TIMEOUT_MS, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, -} from '@lib/constants'; +} from '@shared/constants'; import { authenticate } from '@lib/programs/authenticate'; import { buildSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index c4106e99c..342eaf883 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -21,7 +21,7 @@ import { } from '@lib/agent/agent-interface'; import { isAbsolute, resolve, sep } from 'path'; import { detectNodePackageManagers } from './package-manager.js'; -import { CallType, getSkillsBaseUrl, HAIKU_MODEL } from '@lib/constants'; +import { CallType, getSkillsBaseUrl, HAIKU_MODEL } from '@shared/constants'; import { analytics } from '@utils/analytics'; import type { WizardSession } from '@lib/wizard-session'; import type { WizardRunOptions } from '@utils/types'; diff --git a/src/lib/detection/context.ts b/src/lib/detection/context.ts index 5ecb68925..8e0d845d7 100644 --- a/src/lib/detection/context.ts +++ b/src/lib/detection/context.ts @@ -7,7 +7,7 @@ */ import * as semver from 'semver'; -import { DETECTION_TIMEOUT_MS } from '@lib/constants'; +import { DETECTION_TIMEOUT_MS } from '@shared/constants'; import type { FrameworkConfig } from '@lib/framework-config'; import type { WizardRunOptions } from '@utils/types'; diff --git a/src/lib/detection/framework.ts b/src/lib/detection/framework.ts index 4efd9f6bf..07dfa2c35 100644 --- a/src/lib/detection/framework.ts +++ b/src/lib/detection/framework.ts @@ -6,7 +6,7 @@ * (or undefined). No store mutations, no UI calls. */ -import { Integration, DETECTION_TIMEOUT_MS } from '@lib/constants'; +import { Integration, DETECTION_TIMEOUT_MS } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; /** diff --git a/src/lib/detection/project-scope.ts b/src/lib/detection/project-scope.ts index da4ffaf0a..0cf7ace18 100644 --- a/src/lib/detection/project-scope.ts +++ b/src/lib/detection/project-scope.ts @@ -14,7 +14,7 @@ import { AGENTIC_DETECTION_TIMEOUT_MS, Integration, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, -} from '@lib/constants'; +} from '@shared/constants'; import type { WizardSession } from '@lib/wizard-session'; import { getUI } from '@ui/index'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/framework-config.ts b/src/lib/framework-config.ts index 5bbc7440b..dd33fbe7b 100644 --- a/src/lib/framework-config.ts +++ b/src/lib/framework-config.ts @@ -1,4 +1,4 @@ -import type { Integration } from './constants'; +import type { Integration } from '@shared/constants'; import type { WizardRunOptions } from '@utils/types'; import type { PackageManagerDetector } from './detection/package-manager'; diff --git a/src/lib/gateway-session.ts b/src/lib/gateway-session.ts index 55d50d651..9932f36ad 100644 --- a/src/lib/gateway-session.ts +++ b/src/lib/gateway-session.ts @@ -9,10 +9,10 @@ import { readFileSync } from 'node:fs'; import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; -import { ErrorCodes, WizardError } from '@lib/errors'; -import type { HostResolution } from '@lib/host-resolution'; -import { checkLlmGatewayHealth } from '@lib/health-checks/endpoints'; -import { ServiceHealthStatus } from '@lib/health-checks/types'; +import { ErrorCodes, WizardError } from '@shared/errors'; +import type { HostResolution } from '@shared/host-resolution'; +import { checkLlmGatewayHealth } from '@shared/health-checks/endpoints'; +import { ServiceHealthStatus } from '@shared/health-checks/types'; import { IS_PRODUCTION_BUILD, runtimeEnv } from '@env'; import type { CloudRegion } from '@utils/types'; diff --git a/src/lib/mcp-project-profile.ts b/src/lib/mcp-project-profile.ts index 12a398871..a5b810fcd 100644 --- a/src/lib/mcp-project-profile.ts +++ b/src/lib/mcp-project-profile.ts @@ -25,7 +25,7 @@ */ import axios from 'axios'; -import { WIZARD_USER_AGENT } from './constants'; +import { WIZARD_USER_AGENT } from '@shared/constants'; import { logToFile } from '@utils/debug'; // Time-box every probe call. The scout sits on the critical path between diff --git a/src/lib/mcp-role-prompts.ts b/src/lib/mcp-role-prompts.ts index 5b8921f73..0fbf980dd 100644 --- a/src/lib/mcp-role-prompts.ts +++ b/src/lib/mcp-role-prompts.ts @@ -19,7 +19,7 @@ * 3. Unknown role → DEFAULT_KIT. */ -import type { Integration } from './constants'; +import type { Integration } from '@shared/constants'; import type { ProductPresence, ProjectDataProfile, diff --git a/src/lib/oauth/program-scopes.ts b/src/lib/oauth/program-scopes.ts index 69da4f59a..37ade34e5 100644 --- a/src/lib/oauth/program-scopes.ts +++ b/src/lib/oauth/program-scopes.ts @@ -35,7 +35,7 @@ import type { ProgramId } from '@lib/programs/program-registry'; import { WIZARD_OAUTH_SCOPES, WIZARD_PROVISIONING_SCOPES, -} from '@lib/constants'; +} from '@shared/constants'; /** * Extra scopes the MCP tutorial needs on top of `WIZARD_OAUTH_SCOPES`. diff --git a/src/lib/programs/__tests__/agent-skill.test.ts b/src/lib/programs/__tests__/agent-skill.test.ts index b836f0486..913ce5d01 100644 --- a/src/lib/programs/__tests__/agent-skill.test.ts +++ b/src/lib/programs/__tests__/agent-skill.test.ts @@ -5,7 +5,7 @@ import { } from '@lib/programs/agent-skill/index'; import type { ProgramRun } from '@lib/programs/program-run'; import { buildSession, RunPhase } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; const baseOpts: SkillProgramOptions = { skillId: 'error-tracking-setup', diff --git a/src/lib/programs/__tests__/error-tracking.test.ts b/src/lib/programs/__tests__/error-tracking.test.ts index 5ecef5279..bf49fb9c5 100644 --- a/src/lib/programs/__tests__/error-tracking.test.ts +++ b/src/lib/programs/__tests__/error-tracking.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { ProgramRun } from '@lib/programs/program-run'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import type { AgenticDetectionReport } from '@lib/detection/agentic'; import { detectFramework } from '@lib/detection/index'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { ERROR_TRACKING_TIPS } from '@lib/programs/error-tracking/content/tips'; import { ERROR_TRACKING_PROJECT_PATH_KEY, diff --git a/src/lib/programs/__tests__/flow-traces.test.ts b/src/lib/programs/__tests__/flow-traces.test.ts index 43b532b89..769c1759e 100644 --- a/src/lib/programs/__tests__/flow-traces.test.ts +++ b/src/lib/programs/__tests__/flow-traces.test.ts @@ -18,10 +18,10 @@ import { OutroKind, type WizardSession, } from '@lib/wizard-session'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { HostResolution } from '@lib/host-resolution'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { HostResolution } from '@shared/host-resolution'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { analytics } from '@utils/analytics'; import { PROGRAM_REGISTRY, diff --git a/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts b/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts index 5138af7cb..92f17e703 100644 --- a/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts +++ b/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts @@ -1,7 +1,10 @@ import { refreshAccessTokenIfNeeded } from '../authenticate'; import { refreshAccessToken } from '@utils/oauth'; import { OAuthError } from '@utils/oauth-errors'; -import { isGrantRevoked, resetAuthSessionState } from '@lib/auth-session-state'; +import { + isGrantRevoked, + resetAuthSessionState, +} from '@shared/auth-session-state'; import type { WizardSession, Credentials } from '@lib/wizard-session'; vi.mock('@utils/oauth', () => ({ refreshAccessToken: vi.fn() })); diff --git a/src/lib/programs/__tests__/replay-vision.test.ts b/src/lib/programs/__tests__/replay-vision.test.ts index a9146d2af..c049a66b4 100644 --- a/src/lib/programs/__tests__/replay-vision.test.ts +++ b/src/lib/programs/__tests__/replay-vision.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { replayVisionConfig, REPLAY_VISION_SUPPORTED, diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index c2ad3dead..221828e94 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -2,9 +2,9 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; import { authenticate } from '@lib/programs/authenticate'; import { runProgramAgent } from '../run-agent-legacy'; import { runAgent, RunOutcome, type RunResult } from '@lib/agent/runner'; -import { Harness, Sequence } from '@lib/constants'; +import { Harness, Sequence } from '@shared/constants'; import { buildSession, OutroKind } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import { LoggingUI } from '@ui/logging-ui'; import { setUI } from '@ui'; import { analytics } from '@utils/analytics'; @@ -17,8 +17,8 @@ vi.mock('@env', async (original) => ({ ...(await original()), IS_PRODUCTION_BUILD: false, })); -vi.mock('@lib/local-dev', async (original) => ({ - ...(await original()), +vi.mock('@shared/local-dev', async (original) => ({ + ...(await original()), checkLocalServices: vi.fn().mockResolvedValue(null), })); vi.mock('@utils/environment', async (original) => ({ diff --git a/src/lib/programs/__tests__/self-driving-detect.test.ts b/src/lib/programs/__tests__/self-driving-detect.test.ts index bce8b973a..b7ab13a60 100644 --- a/src/lib/programs/__tests__/self-driving-detect.test.ts +++ b/src/lib/programs/__tests__/self-driving-detect.test.ts @@ -22,7 +22,7 @@ import { PROJECT_MANIFESTS, type AgenticDetectionReport, } from '@lib/detection/agentic'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import { buildSession } from '@lib/wizard-session'; import type { Mock } from 'vitest'; diff --git a/src/lib/programs/__tests__/self-driving-prompt.test.ts b/src/lib/programs/__tests__/self-driving-prompt.test.ts index d23e7c1bc..fefc088c2 100644 --- a/src/lib/programs/__tests__/self-driving-prompt.test.ts +++ b/src/lib/programs/__tests__/self-driving-prompt.test.ts @@ -1,6 +1,6 @@ import { buildSelfDrivingPrompt } from '@lib/programs/self-driving/prompt'; import type { PromptContext } from '@lib/agent/agent-runner'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import type { DetectedSource } from '@lib/warehouse-sources/types'; const ctx: PromptContext = { diff --git a/src/lib/programs/audit/detect.ts b/src/lib/programs/audit/detect.ts index 2cfa299a5..c5e4d4c5e 100644 --- a/src/lib/programs/audit/detect.ts +++ b/src/lib/programs/audit/detect.ts @@ -1,5 +1,5 @@ import type { AbortCase } from '@lib/agent/agent-runner'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; /** `[ABORT] ` cases the audit skill can emit. Reason strings are * defined in the skill's `Abort statuses` section. */ diff --git a/src/lib/programs/audit/types.ts b/src/lib/programs/audit/types.ts index 0a705ce24..a799d0f3b 100644 --- a/src/lib/programs/audit/types.ts +++ b/src/lib/programs/audit/types.ts @@ -5,7 +5,7 @@ import { coerceAuditChecks, type AuditCheck, type AuditStatus, -} from '@lib/audit-ledger'; +} from '@shared/audit-ledger'; // The ledger contract lives in `@lib/audit-ledger`; re-exported so the audit // views and the ledger watcher keep their import path. diff --git a/src/lib/programs/authenticate.ts b/src/lib/programs/authenticate.ts index 8644a328c..109adc9f4 100644 --- a/src/lib/programs/authenticate.ts +++ b/src/lib/programs/authenticate.ts @@ -15,7 +15,7 @@ import type { ProgramId } from '@lib/programs/program-registry'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { refreshAccessToken } from '@utils/oauth'; import { OAuthError } from '@utils/oauth-errors'; -import { markGrantRevoked } from '@lib/auth-session-state'; +import { markGrantRevoked } from '@shared/auth-session-state'; import { analytics, groupsFromUser } from '@utils/analytics'; import { getUI } from '@ui'; import { logToFile } from '@utils/debug'; diff --git a/src/lib/programs/dispatch-family.ts b/src/lib/programs/dispatch-family.ts index a730d8198..ebb199ecd 100644 --- a/src/lib/programs/dispatch-family.ts +++ b/src/lib/programs/dispatch-family.ts @@ -6,14 +6,14 @@ import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; import { agentSkillConfig } from '@lib/programs/program-registry'; import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; import type { ProgramConfig } from '@lib/programs/program-step'; -import { getSkillsBaseUrl } from '@lib/constants'; +import { getSkillsBaseUrl } from '@shared/constants'; import { fetchSkillMenu, type CliEntry } from '@lib/wizard-tools'; import { analytics } from '@utils/analytics'; import { dispatchProgram } from '../../commands/factories/shared'; import type { Command } from '../../commands/command'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; /** * Capture a CLI dispatch error, flush analytics, and exit. The wizard never 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..8e67df613 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/detect.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/detect.ts @@ -18,7 +18,7 @@ import { } from '@utils/bounded-fs'; import type { WizardSession } from '@lib/wizard-session'; import type { AbortCase } from '@lib/agent/agent-runner'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; /** * Skill variants published under the `error-tracking-upload-source-maps` diff --git a/src/lib/programs/error-tracking/detect-agentic.ts b/src/lib/programs/error-tracking/detect-agentic.ts index 5f7fd5268..1967b79c4 100644 --- a/src/lib/programs/error-tracking/detect-agentic.ts +++ b/src/lib/programs/error-tracking/detect-agentic.ts @@ -11,7 +11,7 @@ * - The scan bills to error-tracking. */ -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { resolveProjectDir, type AgenticDetectionReport, diff --git a/src/lib/programs/error-tracking/index.ts b/src/lib/programs/error-tracking/index.ts index 68e7986b5..b28ae779d 100644 --- a/src/lib/programs/error-tracking/index.ts +++ b/src/lib/programs/error-tracking/index.ts @@ -1,4 +1,4 @@ -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { detectFramework } from '@lib/detection/index'; import { scopeInstallDirToProject } from '@lib/detection/project-scope'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; @@ -16,7 +16,7 @@ import type { WizardSession } from '@lib/wizard-session'; import { preinstallPostHogCliOnce } from '@lib/programs/shared/posthog-cli-preinstall'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; const ERROR_TRACKING_REPORT_FILE = 'posthog-error-tracking-report.md'; const ERROR_TRACKING_DOCS_URL = 'https://posthog.com/docs/error-tracking'; diff --git a/src/lib/programs/events-audit/constants.ts b/src/lib/programs/events-audit/constants.ts index a70c343df..8d381e65c 100644 --- a/src/lib/programs/events-audit/constants.ts +++ b/src/lib/programs/events-audit/constants.ts @@ -8,4 +8,4 @@ export { EVENTS_AUDIT_REPORT_FILE as SETUP_REPORT_FILE, EVENT_INVENTORY_FILE, EVENT_INVENTORY_PART_PATTERN, -} from '@lib/constants'; +} from '@shared/constants'; diff --git a/src/lib/programs/mcp-analytics/index.ts b/src/lib/programs/mcp-analytics/index.ts index f9199d76e..b5e79e445 100644 --- a/src/lib/programs/mcp-analytics/index.ts +++ b/src/lib/programs/mcp-analytics/index.ts @@ -1,5 +1,5 @@ import type { AbortCase } from '@lib/agent/agent-runner'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { createSkillProgram } from '@lib/programs/agent-skill/index'; const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; diff --git a/src/lib/programs/posthog-doctor/fetch.ts b/src/lib/programs/posthog-doctor/fetch.ts index b0a5f2e44..f285359de 100644 --- a/src/lib/programs/posthog-doctor/fetch.ts +++ b/src/lib/programs/posthog-doctor/fetch.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { analytics } from '@utils/analytics'; -import { handleApiError } from '@lib/api'; -import { WIZARD_USER_AGENT } from '@lib/constants'; +import { handleApiError } from '@shared/api'; +import { WIZARD_USER_AGENT } from '@shared/constants'; import { HealthIssueListResponseSchema, type HealthIssue } from './types'; export async function fetchHealthIssues( diff --git a/src/lib/programs/posthog-doctor/kind-metadata.ts b/src/lib/programs/posthog-doctor/kind-metadata.ts index 885e1062e..fc9d5859d 100644 --- a/src/lib/programs/posthog-doctor/kind-metadata.ts +++ b/src/lib/programs/posthog-doctor/kind-metadata.ts @@ -1,4 +1,4 @@ -import { POSTHOG_DOCS_URL } from '@lib/constants'; +import { POSTHOG_DOCS_URL } from '@shared/constants'; export interface KindMeta { title: string; diff --git a/src/lib/programs/posthog-integration/constants.ts b/src/lib/programs/posthog-integration/constants.ts index a0bb8d34b..957c41a14 100644 --- a/src/lib/programs/posthog-integration/constants.ts +++ b/src/lib/programs/posthog-integration/constants.ts @@ -4,4 +4,4 @@ * can name it without importing this program. */ -export { EVENT_PLAN_FILE } from '@lib/constants'; +export { EVENT_PLAN_FILE } from '@shared/constants'; diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index 9fc7aab8e..ab8551e21 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -15,12 +15,12 @@ import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; import { scopeInstallDirToProject } from '@lib/detection/project-scope'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; -import { WIZARD_INTERACTION_EVENT_NAME } from '@lib/constants'; +import { ErrorCodes } from '@shared/errors'; +import { WIZARD_INTERACTION_EVENT_NAME } from '@shared/constants'; import { getUI } from '@ui/index'; import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; -import type { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@shared/host-resolution'; import { getDetectedWarehouseSources } from '@lib/programs/warehouse-source/detect'; import { shouldDisableAsk } from '@lib/agent/agent-runner'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; diff --git a/src/lib/programs/program-registry.ts b/src/lib/programs/program-registry.ts index 9bd564bb3..2b001e56c 100644 --- a/src/lib/programs/program-registry.ts +++ b/src/lib/programs/program-registry.ts @@ -11,7 +11,7 @@ */ import type { ProgramConfig } from './program-step.js'; -import { POSTHOG_DOCS_URL } from '../constants.js'; +import { POSTHOG_DOCS_URL } from '@shared/constants.js'; import { posthogIntegrationConfig } from './posthog-integration/index.js'; import { revenueAnalyticsConfig } from './revenue-analytics/index.js'; import { warehouseSourceConfig } from './warehouse-source/index.js'; diff --git a/src/lib/programs/program-step.ts b/src/lib/programs/program-step.ts index 69d0d7cfd..f0983c9e0 100644 --- a/src/lib/programs/program-step.ts +++ b/src/lib/programs/program-step.ts @@ -3,9 +3,9 @@ import type { DiscoveredFeature, TaskNotice, } from '@lib/wizard-session'; -import type { WizardReadinessResult } from '@lib/health-checks/readiness'; +import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { ProgramRun } from '@lib/programs/program-run'; -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import type { FrameworkConfig } from '@lib/framework-config'; import type { ContentBlock } from '@ui/tui/primitives/index'; import type { WizardStore } from '@ui/tui/store'; diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index 046f76e25..9c2fe78b3 100644 --- a/src/lib/programs/replay-vision/index.ts +++ b/src/lib/programs/replay-vision/index.ts @@ -1,5 +1,5 @@ import type { AbortCase } from '@lib/agent/agent-runner'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; import { scopeInstallDirToProject } from '@lib/detection/project-scope'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; @@ -14,7 +14,7 @@ import type { import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index f26bc571b..a3f0c6fe6 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -41,10 +41,10 @@ import { SIGNUP_WIZARD_READINESS_CONFIG, getBlockingServiceKeys, SERVICE_LABELS, -} from '@lib/health-checks/readiness'; +} from '@shared/health-checks/readiness'; import { enableDebugLogs, logToFile, initLogFile } from '@utils/debug'; import { registerCleanup, wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { isNonInteractiveEnvironment } from '@utils/environment'; import { getSkillsBaseUrl, @@ -52,7 +52,7 @@ import { WIZARD_ORCHESTRATOR_FLAG_KEY, WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, type Integration, -} from '@lib/constants'; +} from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { postAuthGateSteps, type ProgramConfig } from './program-step'; import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; diff --git a/src/lib/programs/self-driving/detect-agentic.ts b/src/lib/programs/self-driving/detect-agentic.ts index ddb416941..3822359de 100644 --- a/src/lib/programs/self-driving/detect-agentic.ts +++ b/src/lib/programs/self-driving/detect-agentic.ts @@ -20,7 +20,7 @@ import { toIntegrationCandidates, } from '@lib/detection/project-scope'; import { gatherFrameworkContext } from '@lib/detection/index'; -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import type { WizardSession } from '@lib/wizard-session'; export type { DetectEvent }; diff --git a/src/lib/programs/self-driving/detect.ts b/src/lib/programs/self-driving/detect.ts index d9c74f055..e88aab1c3 100644 --- a/src/lib/programs/self-driving/detect.ts +++ b/src/lib/programs/self-driving/detect.ts @@ -30,7 +30,7 @@ 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 { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/shared/health-check-step.ts b/src/lib/programs/shared/health-check-step.ts index 1c630a52c..8a42179eb 100644 --- a/src/lib/programs/shared/health-check-step.ts +++ b/src/lib/programs/shared/health-check-step.ts @@ -18,7 +18,7 @@ import { WizardReadiness, SIGNUP_WIZARD_READINESS_CONFIG, getBlockingServiceKeys, -} from '@lib/health-checks/readiness'; +} from '@shared/health-checks/readiness'; import { logToFile } from '@utils/debug'; export function healthCheckReady(session: WizardSession): boolean { diff --git a/src/lib/programs/web-analytics-doctor/detect.ts b/src/lib/programs/web-analytics-doctor/detect.ts index 749f4e75d..f1ec56484 100644 --- a/src/lib/programs/web-analytics-doctor/detect.ts +++ b/src/lib/programs/web-analytics-doctor/detect.ts @@ -1,7 +1,7 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; import type { AbortCase } from '@lib/agent/agent-runner'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { findPackageJsons } from '@lib/programs/shared/package-scanning'; export type WebAnalyticsDetectError = diff --git a/src/lib/registry.ts b/src/lib/registry.ts index cd0133aeb..0c1ae0b1c 100644 --- a/src/lib/registry.ts +++ b/src/lib/registry.ts @@ -1,5 +1,5 @@ import type { FrameworkConfig } from './framework-config'; -import { Integration } from './constants'; +import { Integration } from '@shared/constants'; import { NEXTJS_AGENT_CONFIG } from '@frameworks/nextjs/nextjs-wizard-agent'; import { NUXT_AGENT_CONFIG } from '@frameworks/nuxt/nuxt-wizard-agent'; import { VUE_AGENT_CONFIG } from '@frameworks/vue/vue-wizard-agent'; diff --git a/src/lib/runners/__tests__/mint-recovery.test.ts b/src/lib/runners/__tests__/mint-recovery.test.ts index 9a564640e..affbebc70 100644 --- a/src/lib/runners/__tests__/mint-recovery.test.ts +++ b/src/lib/runners/__tests__/mint-recovery.test.ts @@ -7,13 +7,13 @@ import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui'; import { posthogIntegrationConfig } from '@lib/programs/posthog-integration'; import { ScreenId } from '@ui/tui/router'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import { analytics } from '@utils/analytics'; vi.mock('@lib/programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn() })); vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); -vi.mock('@lib/local-dev', async (original) => ({ - ...(await original()), +vi.mock('@shared/local-dev', async (original) => ({ + ...(await original()), getLocalDev: () => ({}), checkLocalServices: () => Promise.resolve(null), })); diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 10ebfa4ca..d09fd9249 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -1,9 +1,13 @@ -import { POSTHOG_DOCS_URL, type Harness, type Sequence } from '@lib/constants'; +import { + POSTHOG_DOCS_URL, + type Harness, + type Sequence, +} from '@shared/constants'; import { checkLocalServices, getLocalDev, POSTHOG_LOCAL_URL, -} from '@lib/local-dev'; +} from '@shared/local-dev'; import type { CloudRegion } from '@utils/types'; import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; @@ -19,7 +23,7 @@ import { classifyRunFailure, detectErrorCode, emitWizardError, -} from '@lib/errors'; +} from '@shared/errors'; import type { OutroData, RunPhase as RunPhaseT } from '@lib/wizard-session'; /** diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index a1f5c5390..a3eaa8f5c 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,4 +1,4 @@ -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import { logToFile, getLogFilePath } from '@utils/debug'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { authenticate } from '@lib/programs/authenticate'; @@ -6,15 +6,15 @@ import { getProgramConfig } from '@lib/programs/program-registry'; import { getAuditChecks } from '@lib/programs/audit/types'; import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; import type { ProgramConfig } from '@lib/programs/program-step'; -import type { Harness, Sequence } from '@lib/constants'; +import type { Harness, Sequence } from '@shared/constants'; import type { startTUI as StartTUIFn } from '@ui/tui/start-tui'; import type { WizardStore } from '@ui/tui/store'; import { OutroKind, type WizardSession } from '@lib/wizard-session'; import type { TaskStreamPush as TaskStreamPushClass } from '@lib/task-stream/task-stream-push'; import { resolveNoTelemetry } from './resolve-no-telemetry'; -import { checkLocalServices, getLocalDev } from '@lib/local-dev'; +import { checkLocalServices, getLocalDev } from '@shared/local-dev'; import { runCleanups } from '@utils/wizard-abort'; -import { classifyRunFailure, emitWizardError } from '@lib/errors'; +import { classifyRunFailure, emitWizardError } from '@shared/errors'; import { isRunFailure } from '@ui/mint-failure'; import { getUI } from '@ui'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/task-stream/__tests__/posthog-destination.test.ts b/src/lib/task-stream/__tests__/posthog-destination.test.ts index 3f5945456..48035d7f6 100644 --- a/src/lib/task-stream/__tests__/posthog-destination.test.ts +++ b/src/lib/task-stream/__tests__/posthog-destination.test.ts @@ -1,7 +1,7 @@ import { PostHogDestination } from '../destinations/posthog'; import { StreamEvent, type TaskStreamUpdate } from '../types'; import { RunPhase, type Credentials } from '../../wizard-session'; -import { HostResolution } from '../../host-resolution'; +import { HostResolution } from '@shared/host-resolution'; const SAMPLE_CREDS: Credentials = { host: HostResolution.fromApiHost('https://us.posthog.com'), diff --git a/src/lib/task-stream/task-stream-push.ts b/src/lib/task-stream/task-stream-push.ts index e7c2515d2..d4bce7af9 100644 --- a/src/lib/task-stream/task-stream-push.ts +++ b/src/lib/task-stream/task-stream-push.ts @@ -36,7 +36,7 @@ import { import { EventPlanWatcher } from './event-plan-watcher'; import { rollUpAuditAreas } from './audit-areas'; import { logToFile } from '@utils/debug'; -import { sanitizeErrorDetail } from '@lib/errors'; +import { sanitizeErrorDetail } from '@shared/errors'; /** Trailing-edge debounce window for non-phase-change emits. */ const DEBOUNCE_MS = 250; diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index bb3b8a621..e90e82f7e 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -10,7 +10,7 @@ * Business logic reads from the session. Never calls a prompt. */ -import { POSTHOG_LOCAL_URL, resolveLocalDev } from './local-dev'; +import { POSTHOG_LOCAL_URL, resolveLocalDev } from '@shared/local-dev'; import { AdditionalFeature, ADDITIONAL_FEATURE_LABELS, @@ -18,11 +18,11 @@ import { type Harness, type Integration, type Sequence, -} from './constants'; +} from '@shared/constants'; import type { FrameworkConfig } from './framework-config'; -import type { WizardReadinessResult } from './health-checks/readiness'; +import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { SettingsConflict } from './agent/claude-settings'; -import type { ApiUser, ApiProject, Credentials } from './api'; +import type { ApiUser, ApiProject, Credentials } from '@shared/api'; import type { CloudRegion } from '@utils/types'; import { OutroKind, diff --git a/src/lib/wizard-spellbook.ts b/src/lib/wizard-spellbook.ts index ef1847433..9477f23f0 100644 --- a/src/lib/wizard-spellbook.ts +++ b/src/lib/wizard-spellbook.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises'; import path from 'path'; -import { getSkillsBaseUrl, POSTHOG_DOCS_URL } from './constants'; +import { getSkillsBaseUrl, POSTHOG_DOCS_URL } from '@shared/constants'; import type { ProgramConfig } from './programs/program-step'; import type { WizardSession } from './wizard-session'; import { diff --git a/src/lib/wizard-tools/mcp.ts b/src/lib/wizard-tools/mcp.ts index 1b48eee2a..339391d48 100644 --- a/src/lib/wizard-tools/mcp.ts +++ b/src/lib/wizard-tools/mcp.ts @@ -19,7 +19,7 @@ import { AUDIT_CHECKS_FILE, type AuditCheck, type AuditStatus, -} from '../audit-ledger'; +} from '@shared/audit-ledger'; import { type WizardAskBridge, isFullyCancelled } from '../wizard-ask-bridge'; import { PUBLISH_HANDOFF_CONTENT_DESCRIPTION, @@ -27,7 +27,7 @@ import { PUBLISH_HANDOFF_TOOL_NAME, publishHandoff, } from './handoff'; -import { createSecretVault, type SecretVault } from '../secret-vault'; +import { createSecretVault, type SecretVault } from '@shared/secret-vault'; import type { ProgressEmitter } from '@lib/agent/progress'; import { buildOrchestratorTools, diff --git a/src/lib/wizard-tools/tools.ts b/src/lib/wizard-tools/tools.ts index 134de05f5..100d8e7f7 100644 --- a/src/lib/wizard-tools/tools.ts +++ b/src/lib/wizard-tools/tools.ts @@ -28,10 +28,10 @@ import { coerceAuditChecks, type AuditCheck, type AuditStatus, -} from '../audit-ledger'; +} from '@shared/audit-ledger'; import { CANCELLED_SENTINEL } from '../wizard-ask-bridge'; -import type { SecretVault } from '../secret-vault'; -import { fetchWithRetry, type RetryOpts } from '../fetch-retry'; +import type { SecretVault } from '@shared/secret-vault'; +import { fetchWithRetry, type RetryOpts } from '@shared/fetch-retry'; // --------------------------------------------------------------------------- // Skill types diff --git a/src/lib/yara-hooks.ts b/src/lib/yara-hooks.ts index 6f97de2ac..e922837f3 100644 --- a/src/lib/yara-hooks.ts +++ b/src/lib/yara-hooks.ts @@ -35,7 +35,7 @@ import type { import { logToFile } from '@utils/debug'; import { readFileHead } from '@utils/bounded-fs'; import { analytics } from '@utils/analytics'; -import { isSkillInstallCommand } from './skill-install'; +import { isSkillInstallCommand } from '@shared/skill-install'; import { highestSeverityMatch, publishBlockingMatch, @@ -53,8 +53,8 @@ import { EVENT_INVENTORY_FILE, EVENT_INVENTORY_PART_PATTERN, EVENT_PLAN_FILE, -} from '@lib/constants'; -import { AUDIT_REPORT_FILE } from '@lib/audit-ledger'; +} from '@shared/constants'; +import { AUDIT_REPORT_FILE } from '@shared/audit-ledger'; // ─── Warlock module accessor ───────────────────────────────────── // Warlock is ESM-only and lazily inits its WASM engine + compiles rules on the diff --git a/src/lib/__tests__/constants.test.ts b/src/shared/__tests__/constants.test.ts similarity index 88% rename from src/lib/__tests__/constants.test.ts rename to src/shared/__tests__/constants.test.ts index 6a5390819..93184d5bd 100644 --- a/src/lib/__tests__/constants.test.ts +++ b/src/shared/__tests__/constants.test.ts @@ -1,4 +1,7 @@ -import { WIZARD_USER_AGENT, wizardUserAgentForProgram } from '@lib/constants'; +import { + WIZARD_USER_AGENT, + wizardUserAgentForProgram, +} from '@shared/constants'; describe('wizardUserAgentForProgram', () => { it('tags the program so the backend can attribute created_via (self-driving → self_driving)', () => { diff --git a/src/lib/__tests__/fetch-retry.test.ts b/src/shared/__tests__/fetch-retry.test.ts similarity index 100% rename from src/lib/__tests__/fetch-retry.test.ts rename to src/shared/__tests__/fetch-retry.test.ts diff --git a/src/lib/__tests__/host-resolution.test.ts b/src/shared/__tests__/host-resolution.test.ts similarity index 97% rename from src/lib/__tests__/host-resolution.test.ts rename to src/shared/__tests__/host-resolution.test.ts index a22d55150..1e47ce9f5 100644 --- a/src/lib/__tests__/host-resolution.test.ts +++ b/src/shared/__tests__/host-resolution.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { HostResolution, mcpUrlFor } from '@lib/host-resolution'; +import { HostResolution, mcpUrlFor } from '@shared/host-resolution'; /** * Contract test for HostResolution — the durable record of what the host family @@ -102,7 +102,7 @@ describe('mcpUrlFor', () => { process.env.MCP_URL = 'https://evil.example.com/mcp'; try { vi.resetModules(); - const fresh = await import('@lib/host-resolution'); + const fresh = await import('@shared/host-resolution'); expect(fresh.mcpUrlFor(false)).toBe('https://mcp.posthog.com/mcp'); expect(fresh.mcpUrlFor(true)).toBe('http://localhost:8787/mcp'); } finally { diff --git a/src/lib/__tests__/local-dev.test.ts b/src/shared/__tests__/local-dev.test.ts similarity index 99% rename from src/lib/__tests__/local-dev.test.ts rename to src/shared/__tests__/local-dev.test.ts index 02ced19c2..de3152734 100644 --- a/src/lib/__tests__/local-dev.test.ts +++ b/src/shared/__tests__/local-dev.test.ts @@ -11,12 +11,12 @@ import { localMcpSkillsNotice, resetLocalDev, resolveLocalDev, -} from '@lib/local-dev'; +} from '@shared/local-dev'; import { getSkillsBaseUrl, GITHUB_SKILLS_BASE_URL, LOCAL_SKILLS_BASE_URL, -} from '@lib/constants'; +} from '@shared/constants'; describe('local dev endpoints', () => { it('pins each service to its own port', () => { diff --git a/src/lib/__tests__/secret-vault.test.ts b/src/shared/__tests__/secret-vault.test.ts similarity index 100% rename from src/lib/__tests__/secret-vault.test.ts rename to src/shared/__tests__/secret-vault.test.ts diff --git a/src/lib/api.ts b/src/shared/api.ts similarity index 100% rename from src/lib/api.ts rename to src/shared/api.ts diff --git a/src/lib/audit-ledger.ts b/src/shared/audit-ledger.ts similarity index 100% rename from src/lib/audit-ledger.ts rename to src/shared/audit-ledger.ts diff --git a/src/lib/auth-session-state.ts b/src/shared/auth-session-state.ts similarity index 100% rename from src/lib/auth-session-state.ts rename to src/shared/auth-session-state.ts diff --git a/src/lib/constants.ts b/src/shared/constants.ts similarity index 100% rename from src/lib/constants.ts rename to src/shared/constants.ts diff --git a/src/lib/errors/__tests__/auth.test.ts b/src/shared/errors/__tests__/auth.test.ts similarity index 100% rename from src/lib/errors/__tests__/auth.test.ts rename to src/shared/errors/__tests__/auth.test.ts diff --git a/src/lib/errors/__tests__/codes.test.ts b/src/shared/errors/__tests__/codes.test.ts similarity index 100% rename from src/lib/errors/__tests__/codes.test.ts rename to src/shared/errors/__tests__/codes.test.ts diff --git a/src/lib/errors/__tests__/detect-map.test.ts b/src/shared/errors/__tests__/detect-map.test.ts similarity index 100% rename from src/lib/errors/__tests__/detect-map.test.ts rename to src/shared/errors/__tests__/detect-map.test.ts diff --git a/src/lib/errors/__tests__/run-failure.test.ts b/src/shared/errors/__tests__/run-failure.test.ts similarity index 100% rename from src/lib/errors/__tests__/run-failure.test.ts rename to src/shared/errors/__tests__/run-failure.test.ts diff --git a/src/lib/errors/__tests__/sanitize.test.ts b/src/shared/errors/__tests__/sanitize.test.ts similarity index 100% rename from src/lib/errors/__tests__/sanitize.test.ts rename to src/shared/errors/__tests__/sanitize.test.ts diff --git a/src/lib/errors/agent-map.ts b/src/shared/errors/agent-map.ts similarity index 92% rename from src/lib/errors/agent-map.ts rename to src/shared/errors/agent-map.ts index d9a46a667..d5b9143e6 100644 --- a/src/lib/errors/agent-map.ts +++ b/src/shared/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/errors/auth.ts b/src/shared/errors/auth.ts similarity index 100% rename from src/lib/errors/auth.ts rename to src/shared/errors/auth.ts diff --git a/src/lib/errors/catalog.ts b/src/shared/errors/catalog.ts similarity index 100% rename from src/lib/errors/catalog.ts rename to src/shared/errors/catalog.ts diff --git a/src/lib/errors/codes.ts b/src/shared/errors/codes.ts similarity index 100% rename from src/lib/errors/codes.ts rename to src/shared/errors/codes.ts diff --git a/src/lib/errors/detect-map.ts b/src/shared/errors/detect-map.ts similarity index 100% rename from src/lib/errors/detect-map.ts rename to src/shared/errors/detect-map.ts diff --git a/src/lib/errors/emit.ts b/src/shared/errors/emit.ts similarity index 100% rename from src/lib/errors/emit.ts rename to src/shared/errors/emit.ts diff --git a/src/lib/errors/index.ts b/src/shared/errors/index.ts similarity index 100% rename from src/lib/errors/index.ts rename to src/shared/errors/index.ts diff --git a/src/lib/errors/run-failure.ts b/src/shared/errors/run-failure.ts similarity index 100% rename from src/lib/errors/run-failure.ts rename to src/shared/errors/run-failure.ts diff --git a/src/lib/errors/sanitize.ts b/src/shared/errors/sanitize.ts similarity index 100% rename from src/lib/errors/sanitize.ts rename to src/shared/errors/sanitize.ts diff --git a/src/lib/errors/skill-map.ts b/src/shared/errors/skill-map.ts similarity index 100% rename from src/lib/errors/skill-map.ts rename to src/shared/errors/skill-map.ts diff --git a/src/lib/errors/types.ts b/src/shared/errors/types.ts similarity index 100% rename from src/lib/errors/types.ts rename to src/shared/errors/types.ts diff --git a/src/lib/errors/wizard-error.ts b/src/shared/errors/wizard-error.ts similarity index 100% rename from src/lib/errors/wizard-error.ts rename to src/shared/errors/wizard-error.ts diff --git a/src/lib/fetch-retry.ts b/src/shared/fetch-retry.ts similarity index 100% rename from src/lib/fetch-retry.ts rename to src/shared/fetch-retry.ts diff --git a/src/lib/health-checks/__tests__/health-checks.test.ts b/src/shared/health-checks/__tests__/health-checks.test.ts similarity index 98% rename from src/lib/health-checks/__tests__/health-checks.test.ts rename to src/shared/health-checks/__tests__/health-checks.test.ts index b23b7e2c4..3812f2c33 100644 --- a/src/lib/health-checks/__tests__/health-checks.test.ts +++ b/src/shared/health-checks/__tests__/health-checks.test.ts @@ -5,12 +5,12 @@ import { evaluateWizardReadiness, ServiceHealthStatus, WizardReadiness, -} from '@lib/health-checks/index'; +} from '@shared/health-checks/index'; import { checkLlmGatewayHealth, fetchEndpointHealth, -} from '@lib/health-checks/endpoints'; -import { SIGNUP_WIZARD_READINESS_CONFIG } from '@lib/health-checks/readiness'; +} from '@shared/health-checks/endpoints'; +import { SIGNUP_WIZARD_READINESS_CONFIG } from '@shared/health-checks/readiness'; const URLS = { githubSkillMenu: diff --git a/src/lib/health-checks/endpoints.ts b/src/shared/health-checks/endpoints.ts similarity index 98% rename from src/lib/health-checks/endpoints.ts rename to src/shared/health-checks/endpoints.ts index f98a9eb7b..b6fb91e62 100644 --- a/src/lib/health-checks/endpoints.ts +++ b/src/shared/health-checks/endpoints.ts @@ -1,4 +1,4 @@ -import { AWS_SKILLS_BASE_URL, GITHUB_SKILLS_BASE_URL } from '@lib/constants'; +import { AWS_SKILLS_BASE_URL, GITHUB_SKILLS_BASE_URL } from '@shared/constants'; import { logToFile } from '@utils/debug'; import { ServiceHealthStatus, type BaseHealthResult } from './types'; diff --git a/src/lib/health-checks/index.ts b/src/shared/health-checks/index.ts similarity index 100% rename from src/lib/health-checks/index.ts rename to src/shared/health-checks/index.ts diff --git a/src/lib/health-checks/readiness.ts b/src/shared/health-checks/readiness.ts similarity index 100% rename from src/lib/health-checks/readiness.ts rename to src/shared/health-checks/readiness.ts diff --git a/src/lib/health-checks/testme.md b/src/shared/health-checks/testme.md similarity index 100% rename from src/lib/health-checks/testme.md rename to src/shared/health-checks/testme.md diff --git a/src/lib/health-checks/types.ts b/src/shared/health-checks/types.ts similarity index 100% rename from src/lib/health-checks/types.ts rename to src/shared/health-checks/types.ts diff --git a/src/lib/host-resolution.ts b/src/shared/host-resolution.ts similarity index 99% rename from src/lib/host-resolution.ts rename to src/shared/host-resolution.ts index f55f1b4bc..b7be6a4e3 100644 --- a/src/lib/host-resolution.ts +++ b/src/shared/host-resolution.ts @@ -24,7 +24,7 @@ import { resolveBaseUrl, } from '@utils/urls'; import { IS_PRODUCTION_BUILD, runtimeEnv } from '@env'; -import { MCP_LOCAL_URL } from '@lib/local-dev'; +import { MCP_LOCAL_URL } from '@shared/local-dev'; import type { CloudRegion } from '@utils/types'; // The wizard's client gets CLI mode (a single `exec` tool) by server default, diff --git a/src/lib/local-dev.ts b/src/shared/local-dev.ts similarity index 100% rename from src/lib/local-dev.ts rename to src/shared/local-dev.ts diff --git a/src/lib/secret-vault.ts b/src/shared/secret-vault.ts similarity index 100% rename from src/lib/secret-vault.ts rename to src/shared/secret-vault.ts diff --git a/src/lib/skill-install.ts b/src/shared/skill-install.ts similarity index 100% rename from src/lib/skill-install.ts rename to src/shared/skill-install.ts diff --git a/src/lib/status-history.ts b/src/shared/status-history.ts similarity index 100% rename from src/lib/status-history.ts rename to src/shared/status-history.ts diff --git a/src/shared/utils/__tests__/analytics.test.ts b/src/shared/utils/__tests__/analytics.test.ts index 347424ec1..140065508 100644 --- a/src/shared/utils/__tests__/analytics.test.ts +++ b/src/shared/utils/__tests__/analytics.test.ts @@ -1,9 +1,9 @@ import { Analytics, groupsFromUser, sessionProperties } from '@utils/analytics'; import { PostHog } from 'posthog-node'; import { v4 as uuidv4 } from 'uuid'; -import { ANALYTICS_TEAM_TAG, WIZARD_FLAG_KEYS } from '@lib/constants'; -import { VERSION } from '@lib/version'; -import type { ApiUser } from '@lib/api'; +import { ANALYTICS_TEAM_TAG, WIZARD_FLAG_KEYS } from '@shared/constants'; +import { VERSION } from '@shared/version'; +import type { ApiUser } from '@shared/api'; import { buildSession, DiscoveredFeature, diff --git a/src/shared/utils/__tests__/ci-region.test.ts b/src/shared/utils/__tests__/ci-region.test.ts index d174852a5..fd7e69a99 100644 --- a/src/shared/utils/__tests__/ci-region.test.ts +++ b/src/shared/utils/__tests__/ci-region.test.ts @@ -1,6 +1,6 @@ import { getOrAskForProjectData } from '@utils/setup-utils'; import { detectRegion } from '@utils/urls'; -import { fetchProjectData, fetchUserData } from '@lib/api'; +import { fetchProjectData, fetchUserData } from '@shared/api'; import { performOAuthFlow } from '@utils/oauth'; vi.mock('@ui', () => ({ @@ -15,7 +15,7 @@ vi.mock('@utils/urls', () => ({ getUiHostFromHost: (host: string) => host, resolveBaseUrl: (baseUrl?: string) => baseUrl, })); -vi.mock('@lib/api', () => ({ +vi.mock('@shared/api', () => ({ fetchProjectData: vi.fn(), fetchUserData: vi.fn(), })); diff --git a/src/shared/utils/__tests__/oauth-errors.test.ts b/src/shared/utils/__tests__/oauth-errors.test.ts index 5e0068245..dcbc46987 100644 --- a/src/shared/utils/__tests__/oauth-errors.test.ts +++ b/src/shared/utils/__tests__/oauth-errors.test.ts @@ -11,7 +11,7 @@ import { ISSUES_URL, POSTHOG_STATUS_PAGE_URL, WIZARD_CONTACT_EMAIL, -} from '@lib/constants'; +} from '@shared/constants'; describe('OAuthError', () => { it('keeps the exact legacy message shape so fingerprint parsing still works', () => { diff --git a/src/shared/utils/__tests__/oauth-refresh.test.ts b/src/shared/utils/__tests__/oauth-refresh.test.ts index 94e775287..d3fafdd39 100644 --- a/src/shared/utils/__tests__/oauth-refresh.test.ts +++ b/src/shared/utils/__tests__/oauth-refresh.test.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { refreshAccessToken } from '@utils/oauth'; -import { POSTHOG_PROXY_CLIENT_ID } from '@lib/constants'; +import { POSTHOG_PROXY_CLIENT_ID } from '@shared/constants'; vi.mock('axios'); // No base-URL override resolves to prod routing (kills IS_DEV's implicit localhost). diff --git a/src/shared/utils/__tests__/oauth.test.ts b/src/shared/utils/__tests__/oauth.test.ts index 6c60212ac..6466d36b4 100644 --- a/src/shared/utils/__tests__/oauth.test.ts +++ b/src/shared/utils/__tests__/oauth.test.ts @@ -9,7 +9,7 @@ import { import { WIZARD_OAUTH_SCOPES, WIZARD_PROVISIONING_SCOPES, -} from '@lib/constants'; +} from '@shared/constants'; import { getOAuthScopesForProgram } from '@lib/oauth/program-scopes'; describe('extractOAuthCode', () => { diff --git a/src/shared/utils/analytics.ts b/src/shared/utils/analytics.ts index f494fc439..c5a67fe3f 100644 --- a/src/shared/utils/analytics.ts +++ b/src/shared/utils/analytics.ts @@ -4,16 +4,16 @@ import { ANALYTICS_POSTHOG_PUBLIC_PROJECT_WRITE_KEY, ANALYTICS_TEAM_TAG, WIZARD_FLAG_KEYS, -} from '@lib/constants'; +} from '@shared/constants'; import { reportableDiscoveredFeatures, reportablePosthogSdkDetected, type WizardSession, } from '@lib/wizard-session'; -import type { ApiUser } from '@lib/api'; +import type { ApiUser } from '@shared/api'; import { v4 as uuidv4 } from 'uuid'; import { IS_PRODUCTION_BUILD, RUN_SURFACE, TASK_ID, TASK_RUN_ID } from '@env'; -import { VERSION } from '@lib/version'; +import { VERSION } from '@shared/version'; import { debug, logToFile } from './debug'; import { applyCiFlagOverrides } from './ci-flag-overrides'; diff --git a/src/shared/utils/environment.ts b/src/shared/utils/environment.ts index 58589c869..e639afa26 100644 --- a/src/shared/utils/environment.ts +++ b/src/shared/utils/environment.ts @@ -7,7 +7,7 @@ const readEnv = import { tryGetPackageJson } from './setup-utils'; import type { WizardRunOptions } from './types'; import { boundedGlob } from './bounded-fs'; -import { IS_DEV } from '@lib/constants'; +import { IS_DEV } from '@shared/constants'; export function isNonInteractiveEnvironment(): boolean { if (IS_DEV) { diff --git a/src/shared/utils/oauth-errors.ts b/src/shared/utils/oauth-errors.ts index e95f7f751..3a9f28363 100644 --- a/src/shared/utils/oauth-errors.ts +++ b/src/shared/utils/oauth-errors.ts @@ -14,7 +14,7 @@ import { ISSUES_URL, POSTHOG_STATUS_PAGE_URL, WIZARD_CONTACT_EMAIL, -} from '@lib/constants'; +} from '@shared/constants'; /** * A structured OAuth failure (RFC 6749 §4.1.2.1 authorize errors, §5.2 token diff --git a/src/shared/utils/oauth.ts b/src/shared/utils/oauth.ts index 0fc53e78f..dda07db81 100644 --- a/src/shared/utils/oauth.ts +++ b/src/shared/utils/oauth.ts @@ -11,7 +11,7 @@ import { POSTHOG_DEV_CLIENT_ID, POSTHOG_PROXY_CLIENT_ID, WIZARD_USER_AGENT, -} from '@lib/constants'; +} from '@shared/constants'; import { getOAuthUrl, resolveBaseUrl } from './urls'; import { abort } from './setup-utils'; import { openTrackedLink, withUtm } from './links'; diff --git a/src/shared/utils/provisioning.ts b/src/shared/utils/provisioning.ts index 39e19d73f..ad24cd7ec 100644 --- a/src/shared/utils/provisioning.ts +++ b/src/shared/utils/provisioning.ts @@ -16,11 +16,11 @@ import { POSTHOG_US_CLIENT_ID, WIZARD_PROVISIONING_SCOPES, WIZARD_USER_AGENT, -} from '@lib/constants'; +} from '@shared/constants'; import { resolveBaseUrl } from './urls'; import { logToFile } from './debug'; import { analytics } from './analytics'; -import type { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@shared/host-resolution'; const API_VERSION = '0.1d'; diff --git a/src/shared/utils/setup-utils.ts b/src/shared/utils/setup-utils.ts index 89a8c9ee5..eb434f8ef 100644 --- a/src/shared/utils/setup-utils.ts +++ b/src/shared/utils/setup-utils.ts @@ -14,7 +14,7 @@ import { } from './package-manager'; import type { CloudRegion, WizardRunOptions } from './types'; import { getDeclaredVersion } from './package-json'; -import { DUMMY_PROJECT_API_KEY, ISSUES_URL } from '@lib/constants'; +import { DUMMY_PROJECT_API_KEY, ISSUES_URL } from '@shared/constants'; import { getOAuthScopesForProgram, getProvisioningScopesForProgram, @@ -22,7 +22,7 @@ import { import type { ProgramId } from '@lib/programs/program-registry'; import { analytics } from './analytics'; import { getUI } from '@ui'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import { assertWizardCompletionScope, missingOAuthScopes, @@ -38,7 +38,7 @@ import { fetchProjectData, type ApiUser, type ApiProject, -} from '@lib/api'; +} from '@shared/api'; import { versionSatisfiesRange } from './semver'; import { wizardAbort } from './wizard-abort'; import { OutroKind } from '@lib/wizard-session'; diff --git a/src/shared/utils/types.ts b/src/shared/utils/types.ts index d605a3e76..e553ae65e 100644 --- a/src/shared/utils/types.ts +++ b/src/shared/utils/types.ts @@ -1,4 +1,4 @@ -import type { Harness, Sequence } from '@lib/constants'; +import type { Harness, Sequence } from '@shared/constants'; export type CloudRegion = 'us' | 'eu'; diff --git a/src/shared/utils/urls.ts b/src/shared/utils/urls.ts index 3a0b02d9c..bb10a19af 100644 --- a/src/shared/utils/urls.ts +++ b/src/shared/utils/urls.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { IS_DEV, WIZARD_USER_AGENT } from '@lib/constants'; +import { IS_DEV, WIZARD_USER_AGENT } from '@shared/constants'; import type { CloudRegion } from './types'; /** diff --git a/src/shared/utils/wizard-abort.ts b/src/shared/utils/wizard-abort.ts index f5973a745..5b55b6864 100644 --- a/src/shared/utils/wizard-abort.ts +++ b/src/shared/utils/wizard-abort.ts @@ -11,8 +11,12 @@ 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 { WizardError, emitWizardError, sanitizeErrorDetail } from '@lib/errors'; +import type { ErrorCode } from '@shared/errors'; +import { + WizardError, + emitWizardError, + sanitizeErrorDetail, +} from '@shared/errors'; // Still importable from here; the class lives with the error codes. export { WizardError }; diff --git a/src/steps/add-mcp-server-to-clients/index.ts b/src/steps/add-mcp-server-to-clients/index.ts index 9dfcd10ee..fcc85a83b 100644 --- a/src/steps/add-mcp-server-to-clients/index.ts +++ b/src/steps/add-mcp-server-to-clients/index.ts @@ -1,4 +1,4 @@ -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import type { CloudRegion } from '@utils/types'; import { withProgress } from '../../telemetry'; import { analytics } from '@utils/analytics'; diff --git a/src/steps/add-or-update-environment-variables.ts b/src/steps/add-or-update-environment-variables.ts index 67787dfa8..f4d5d20d4 100644 --- a/src/steps/add-or-update-environment-variables.ts +++ b/src/steps/add-or-update-environment-variables.ts @@ -1,4 +1,4 @@ -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import { withProgress } from '../telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; diff --git a/src/steps/run-prettier.ts b/src/steps/run-prettier.ts index 61a4d165e..95078bdd9 100644 --- a/src/steps/run-prettier.ts +++ b/src/steps/run-prettier.ts @@ -1,4 +1,4 @@ -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import { withProgress } from '../telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; diff --git a/src/steps/upload-environment-variables/index.ts b/src/steps/upload-environment-variables/index.ts index 3a15081b7..239287573 100644 --- a/src/steps/upload-environment-variables/index.ts +++ b/src/steps/upload-environment-variables/index.ts @@ -1,4 +1,4 @@ -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import { withProgress } from '../../telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index 30ff56dac..0d8693cdd 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -12,13 +12,13 @@ import { type TokenUsageDelta, } from './wizard-ui'; import type { SettingsConflict } from '@lib/agent/claude-settings'; -import type { ApiUser } from '@lib/api'; -import { OAUTH_TIMEOUT_MS } from '@lib/constants'; +import type { ApiUser } from '@shared/api'; +import { OAUTH_TIMEOUT_MS } from '@shared/constants'; import { type WizardReadinessResult, getBlockingServiceKeys, SERVICE_LABELS, -} from '@lib/health-checks/readiness'; +} from '@shared/health-checks/readiness'; import type { AskAnswers, Credentials, diff --git a/src/ui/tui/__tests__/MintFailureScreen.test.tsx b/src/ui/tui/__tests__/MintFailureScreen.test.tsx index ffbb25578..42a224d76 100644 --- a/src/ui/tui/__tests__/MintFailureScreen.test.tsx +++ b/src/ui/tui/__tests__/MintFailureScreen.test.tsx @@ -7,7 +7,7 @@ import { } from '../screens/MintFailureScreen'; import { KeyboardHintsProvider } from '../hooks/useKeyboardHints'; import { OutroKind } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import { ScreenId } from '../router'; vi.mock('ink', () => diff --git a/src/ui/tui/__tests__/exit-line.test.ts b/src/ui/tui/__tests__/exit-line.test.ts index 05db612ca..604bac57f 100644 --- a/src/ui/tui/__tests__/exit-line.test.ts +++ b/src/ui/tui/__tests__/exit-line.test.ts @@ -1,7 +1,7 @@ import { getExitLine } from '@ui/tui/exit-line'; import { WizardStore, Program } from '@ui/tui/store'; import { OutroKind } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; vi.mock('@utils/analytics.js', () => ({ analytics: { diff --git a/src/ui/tui/__tests__/frames.test.tsx b/src/ui/tui/__tests__/frames.test.tsx index 2a2b805f6..33f281f11 100644 --- a/src/ui/tui/__tests__/frames.test.tsx +++ b/src/ui/tui/__tests__/frames.test.tsx @@ -44,7 +44,7 @@ vi.mock('@utils/setup-utils', async (actual) => ({ ...(await actual>()), getOrAskForProjectData: vi.fn(pending), })); -vi.mock('@lib/api', async (actual) => ({ +vi.mock('@shared/api', async (actual) => ({ ...(await actual>()), fetchUserData: vi.fn(pending), fetchSlackConnected: vi.fn(pending), @@ -108,16 +108,16 @@ import { RunPhase, McpOutcome, } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; -import { Integration } from '@lib/constants'; +import { HostResolution } from '@shared/host-resolution'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import type { FrameworkConfig } from '@lib/framework-config'; import { Program, type ProgramId } from '@lib/programs/program-registry'; import { WizardReadiness, type WizardReadinessResult, -} from '@lib/health-checks/readiness'; -import { ServiceHealthStatus } from '@lib/health-checks/types'; +} from '@shared/health-checks/readiness'; +import { ServiceHealthStatus } from '@shared/health-checks/types'; import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/detect'; import { AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; import { AUDIT_SEED_CHECKS } from '@lib/programs/audit/seed'; diff --git a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx index 790487197..da8161d06 100644 --- a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx +++ b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx @@ -24,10 +24,10 @@ import { OutroKind, type WizardSession, } from '@lib/wizard-session'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { HostResolution } from '@lib/host-resolution'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { HostResolution } from '@shared/host-resolution'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/detect'; import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; import { ScreenContainer } from '../primitives/ScreenContainer'; @@ -61,8 +61,8 @@ vi.mock('@utils/clipboard', async (importOriginal) => ({ openInBrowser: vi.fn().mockResolvedValue(false), })); vi.mock('opn', () => ({ default: vi.fn() })); -vi.mock('@lib/api', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@shared/api', async (importOriginal) => ({ + ...(await importOriginal()), fetchSlackConnected: vi.fn().mockResolvedValue(false), fetchUserData: vi.fn(() => new Promise(() => undefined)), })); diff --git a/src/ui/tui/__tests__/programs.test.ts b/src/ui/tui/__tests__/programs.test.ts index 6030b70d2..7df542c4a 100644 --- a/src/ui/tui/__tests__/programs.test.ts +++ b/src/ui/tui/__tests__/programs.test.ts @@ -1,5 +1,5 @@ import { buildSession, McpOutcome, RunPhase } from '@lib/wizard-session'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { PROGRAM_SEQUENCES, ScreenId } from '@ui/tui/screen-sequences'; import { Program, type ProgramId } from '@lib/programs/program-registry'; diff --git a/src/ui/tui/__tests__/router.test.ts b/src/ui/tui/__tests__/router.test.ts index 2d893da7e..89b94150f 100644 --- a/src/ui/tui/__tests__/router.test.ts +++ b/src/ui/tui/__tests__/router.test.ts @@ -4,10 +4,10 @@ import { OutroKind, RunPhase, } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { HostResolution } from '@shared/host-resolution'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { WizardRouter, ScreenId, Overlay, Program } from '@ui/tui/router'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts index 509b030bb..2cb4f5e06 100644 --- a/src/ui/tui/__tests__/store-invariants.test.ts +++ b/src/ui/tui/__tests__/store-invariants.test.ts @@ -26,9 +26,9 @@ import { } from '@lib/wizard-session'; import { EXPANDED_COUNT } from '@ui/tui/constants'; import { PROGRAM_SEQUENCES } from '@ui/tui/screen-sequences'; -import { WizardReadiness } from '@lib/health-checks/readiness'; -import { HostResolution } from '@lib/host-resolution'; -import { Integration } from '@lib/constants'; +import { WizardReadiness } from '@shared/health-checks/readiness'; +import { HostResolution } from '@shared/host-resolution'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { analytics } from '@utils/analytics'; import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; @@ -45,7 +45,7 @@ vi.mock('@utils/analytics.js', () => ({ sessionProperties: vi.fn(() => ({})), })); -vi.mock('../../../lib/health-checks/readiness.js', () => ({ +vi.mock('@shared/health-checks/readiness.js', () => ({ evaluateWizardReadiness: vi.fn().mockResolvedValue({ decision: 'yes', health: {}, diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts index fbae262d6..ed8cddf1c 100644 --- a/src/ui/tui/__tests__/store.test.ts +++ b/src/ui/tui/__tests__/store.test.ts @@ -13,10 +13,10 @@ import { EXPANDED_COUNT } from '@ui/tui/constants'; import { WizardReadiness, evaluateWizardReadiness, -} from '@lib/health-checks/readiness'; +} from '@shared/health-checks/readiness'; import { buildSession } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; -import { Integration } from '@lib/constants'; +import { HostResolution } from '@shared/host-resolution'; +import { Integration } from '@shared/constants'; import { analytics } from '@utils/analytics'; import { getProgramConfig } from '@lib/programs/program-registry'; @@ -30,7 +30,7 @@ vi.mock('@utils/analytics.js', () => ({ sessionProperties: vi.fn(() => ({})), })); -vi.mock('../../../lib/health-checks/readiness.js', () => ({ +vi.mock('@shared/health-checks/readiness.js', () => ({ evaluateWizardReadiness: vi.fn().mockResolvedValue({ decision: 'yes', health: {}, diff --git a/src/ui/tui/components/PrivacyPanel.tsx b/src/ui/tui/components/PrivacyPanel.tsx index c938dac67..b4357a4bb 100644 --- a/src/ui/tui/components/PrivacyPanel.tsx +++ b/src/ui/tui/components/PrivacyPanel.tsx @@ -22,7 +22,7 @@ import { POSTHOG_ORG_AI_SETTINGS_URL, POSTHOG_PRIVACY_URL, POSTHOG_TERMS_URL, -} from '@lib/constants'; +} from '@shared/constants'; import { analytics } from '@utils/analytics'; /** diff --git a/src/ui/tui/components/ServiceHealthList.tsx b/src/ui/tui/components/ServiceHealthList.tsx index 99ac8c7da..a630571d6 100644 --- a/src/ui/tui/components/ServiceHealthList.tsx +++ b/src/ui/tui/components/ServiceHealthList.tsx @@ -5,8 +5,8 @@ import { ServiceHealthStatus, type AllServicesHealth, type HealthCheckKey, -} from '@lib/health-checks/types'; -import { SERVICE_LABELS } from '@lib/health-checks/readiness'; +} from '@shared/health-checks/types'; +import { SERVICE_LABELS } from '@shared/health-checks/readiness'; import { Icons } from '@ui/tui/styles'; function statusIcon(status: ServiceHealthStatus): { diff --git a/src/ui/tui/constants.ts b/src/ui/tui/constants.ts index ae420b5c3..c2da8eea7 100644 --- a/src/ui/tui/constants.ts +++ b/src/ui/tui/constants.ts @@ -6,4 +6,4 @@ */ export const COLLAPSED_COUNT = 2; -export { MAX_STATUS_MESSAGES as EXPANDED_COUNT } from '@lib/status-history'; +export { MAX_STATUS_MESSAGES as EXPANDED_COUNT } from '@shared/status-history'; diff --git a/src/ui/tui/hooks/__tests__/useGithubConnection.test.ts b/src/ui/tui/hooks/__tests__/useGithubConnection.test.ts index 96ba780e8..c03de350b 100644 --- a/src/ui/tui/hooks/__tests__/useGithubConnection.test.ts +++ b/src/ui/tui/hooks/__tests__/useGithubConnection.test.ts @@ -1,7 +1,7 @@ import { fetchLoginUrl } from '@ui/tui/hooks/useGithubConnection'; import { requestDeepLink } from '@utils/provisioning'; import type { WizardSession, Credentials } from '@lib/wizard-session'; -import type { HostResolution } from '@lib/host-resolution'; +import type { HostResolution } from '@shared/host-resolution'; vi.mock('@utils/provisioning', () => ({ requestDeepLink: vi.fn() })); diff --git a/src/ui/tui/hooks/useGithubConnection.ts b/src/ui/tui/hooks/useGithubConnection.ts index 0c68aa427..166df92bf 100644 --- a/src/ui/tui/hooks/useGithubConnection.ts +++ b/src/ui/tui/hooks/useGithubConnection.ts @@ -11,7 +11,7 @@ import { useEffect } from 'react'; import type { WizardStore } from '@ui/tui/store'; import type { WizardSession } from '@lib/wizard-session'; -import { fetchGithubConnected } from '@lib/api'; +import { fetchGithubConnected } from '@shared/api'; import { requestDeepLink } from '@utils/provisioning'; import { analytics } from '@utils/analytics'; diff --git a/src/ui/tui/ink-ui.ts b/src/ui/tui/ink-ui.ts index 60c9f27da..f36f3cc60 100644 --- a/src/ui/tui/ink-ui.ts +++ b/src/ui/tui/ink-ui.ts @@ -14,8 +14,8 @@ import type { } from '@ui/wizard-ui'; import type { WizardStore } from './store.js'; import type { SettingsConflict } from '@lib/agent/claude-settings'; -import type { WizardReadinessResult } from '@lib/health-checks/readiness'; -import type { ApiUser } from '@lib/api'; +import type { WizardReadinessResult } from '@shared/health-checks/readiness'; +import type { ApiUser } from '@shared/api'; import type { AskAnswers, Credentials, diff --git a/src/ui/tui/playground/demos/AiOptInDemo.tsx b/src/ui/tui/playground/demos/AiOptInDemo.tsx index 97897e219..4fb99d37a 100644 --- a/src/ui/tui/playground/demos/AiOptInDemo.tsx +++ b/src/ui/tui/playground/demos/AiOptInDemo.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; import { Box, Text } from 'ink'; import { WizardStore } from '@ui/tui/store'; import { AiOptInRequiredScreen } from '@ui/tui/screens/AiOptInRequiredScreen'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; type Variant = 'admin' | 'non-admin'; diff --git a/src/ui/tui/playground/demos/EndScreensDemo.tsx b/src/ui/tui/playground/demos/EndScreensDemo.tsx index 7fc165ba6..4038c689e 100644 --- a/src/ui/tui/playground/demos/EndScreensDemo.tsx +++ b/src/ui/tui/playground/demos/EndScreensDemo.tsx @@ -27,7 +27,7 @@ import { SlackConnectScreen } from '@ui/tui/screens/SlackConnectScreen'; import { OutroScreen } from '@ui/tui/screens/OutroScreen'; import { Colors } from '@ui/tui/styles'; import { OutroKind, type OutroData } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; const VIEWS = ['slack-connect', 'outro'] as const; type View = (typeof VIEWS)[number]; diff --git a/src/ui/tui/playground/demos/HealthCheckDemo.tsx b/src/ui/tui/playground/demos/HealthCheckDemo.tsx index a4e12b936..eb407a1a4 100644 --- a/src/ui/tui/playground/demos/HealthCheckDemo.tsx +++ b/src/ui/tui/playground/demos/HealthCheckDemo.tsx @@ -5,9 +5,9 @@ import { Box, Text } from 'ink'; import { LoadingBox, ModalOverlay } from '@ui/tui/primitives/index'; import { Icons } from '@ui/tui/styles'; import { ServiceHealthList } from '@ui/tui/components/ServiceHealthList'; -import { getBlockingServiceKeys } from '@lib/health-checks/readiness'; -import { ServiceHealthStatus } from '@lib/health-checks/types'; -import type { AllServicesHealth } from '@lib/health-checks/types'; +import { getBlockingServiceKeys } from '@shared/health-checks/readiness'; +import { ServiceHealthStatus } from '@shared/health-checks/types'; +import type { AllServicesHealth } from '@shared/health-checks/types'; const MOCK_CONFIRMED_OUTAGE: AllServicesHealth = { skillsOrigin: { status: ServiceHealthStatus.Down }, diff --git a/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx b/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx index 59aaa256c..a7e4d5b00 100644 --- a/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx +++ b/src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx @@ -36,9 +36,9 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { McpSuggestedPromptsScreen } from '@ui/tui/screens/McpSuggestedPromptsScreen'; import { Colors } from '@ui/tui/styles'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { McpOutcome } from '@lib/wizard-session'; -import { HostResolution } from '@lib/host-resolution'; +import { HostResolution } from '@shared/host-resolution'; import { TAILORED_ROLES } from '@lib/mcp-role-prompts'; import { assembleProfile, diff --git a/src/ui/tui/playground/start-playground.ts b/src/ui/tui/playground/start-playground.ts index 1d59c495e..179ec415d 100644 --- a/src/ui/tui/playground/start-playground.ts +++ b/src/ui/tui/playground/start-playground.ts @@ -6,8 +6,8 @@ import { render } from 'ink'; import { createElement } from 'react'; import { WizardStore } from '@ui/tui/store'; import { PlaygroundApp } from './PlaygroundApp.js'; -import { HostResolution } from '@lib/host-resolution'; -import { WizardReadiness } from '@lib/health-checks/readiness'; +import { HostResolution } from '@shared/host-resolution'; +import { WizardReadiness } from '@shared/health-checks/readiness'; import { enterDarkTerminal, releaseTerminal } from '../terminal.js'; export function startPlayground(version: string): void { diff --git a/src/ui/tui/screens/AiOptInRequiredScreen.tsx b/src/ui/tui/screens/AiOptInRequiredScreen.tsx index ae74821ad..2018af56f 100644 --- a/src/ui/tui/screens/AiOptInRequiredScreen.tsx +++ b/src/ui/tui/screens/AiOptInRequiredScreen.tsx @@ -20,8 +20,8 @@ import type { WizardStore } from '@ui/tui/store'; import { useKeyBindings } from '@ui/tui/hooks/useKeyBindings'; import { Colors } from '@ui/tui/styles'; import { useSkillEntry } from '@ui/tui/screens/SkillSourceInfo'; -import { fetchUserData } from '@lib/api'; -import { CONTEXT_MILL_RELEASES_URL, POSTHOG_APP_URL } from '@lib/constants'; +import { fetchUserData } from '@shared/api'; +import { CONTEXT_MILL_RELEASES_URL, POSTHOG_APP_URL } from '@shared/constants'; import { analytics } from '@utils/analytics'; import { LoadingBox } from '@ui/tui/primitives/index'; diff --git a/src/ui/tui/screens/KeepSkillsScreen.tsx b/src/ui/tui/screens/KeepSkillsScreen.tsx index dfe94a094..5fcd45610 100644 --- a/src/ui/tui/screens/KeepSkillsScreen.tsx +++ b/src/ui/tui/screens/KeepSkillsScreen.tsx @@ -17,7 +17,7 @@ const WIZARD_MARKER = '.posthog-wizard'; import type { WizardStore } from '@ui/tui/store'; import { ConfirmationInput } from '@ui/tui/primitives/index'; import { Colors } from '@ui/tui/styles'; -import { CONTEXT_MILL_URL } from '@lib/constants'; +import { CONTEXT_MILL_URL } from '@shared/constants'; interface KeepSkillsScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx b/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx index 05b010920..c8d7ff9d7 100644 --- a/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx +++ b/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx @@ -69,7 +69,7 @@ import { isKnownCloudHost, type ProjectDataProfile, } from '@lib/mcp-project-profile'; -import type { Integration } from '@lib/constants'; +import type { Integration } from '@shared/constants'; import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; import type { diff --git a/src/ui/tui/screens/PortConflictScreen.tsx b/src/ui/tui/screens/PortConflictScreen.tsx index b110caabf..abb30178c 100644 --- a/src/ui/tui/screens/PortConflictScreen.tsx +++ b/src/ui/tui/screens/PortConflictScreen.tsx @@ -7,7 +7,7 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; -import { OAUTH_PORTS } from '@lib/constants'; +import { OAUTH_PORTS } from '@shared/constants'; import { ConfirmationInput, ModalOverlay } from '@ui/tui/primitives/index'; interface PortConflictScreenProps { diff --git a/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx b/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx index 3da96b496..e4331f473 100644 --- a/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx +++ b/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx @@ -12,7 +12,7 @@ import { Box, Text } from 'ink'; import type { ReactNode } from 'react'; import { useState, useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { getCommandPath, getLaunchablePrograms, diff --git a/src/ui/tui/screens/SelfDrivingIntegrationCheckScreen.tsx b/src/ui/tui/screens/SelfDrivingIntegrationCheckScreen.tsx index ec3179d07..46d8423b1 100644 --- a/src/ui/tui/screens/SelfDrivingIntegrationCheckScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingIntegrationCheckScreen.tsx @@ -26,7 +26,7 @@ import { PRIVACY_PANEL_LABEL, } from '@ui/tui/components/PrivacyPanel'; import { IntroScreenLayout } from '@ui/tui/screens/IntroScreenLayout'; -import { POSTHOG_PRIVACY_URL, POSTHOG_TERMS_URL } from '@lib/constants'; +import { POSTHOG_PRIVACY_URL, POSTHOG_TERMS_URL } from '@shared/constants'; import { Colors } from '@ui/tui/styles'; interface SelfDrivingIntegrationCheckScreenProps { diff --git a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx index a5327232f..110dbc8f0 100644 --- a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx @@ -14,7 +14,7 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; -import { Integration } from '@lib/constants'; +import { Integration } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; import { diff --git a/src/ui/tui/screens/SessionTimeoutScreen.tsx b/src/ui/tui/screens/SessionTimeoutScreen.tsx index 4f928f820..944b80b3d 100644 --- a/src/ui/tui/screens/SessionTimeoutScreen.tsx +++ b/src/ui/tui/screens/SessionTimeoutScreen.tsx @@ -11,7 +11,7 @@ import { Box, Text } from 'ink'; import { useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { Colors } from '@ui/tui/styles'; -import { OAUTH_TIMEOUT_MS } from '@lib/constants'; +import { OAUTH_TIMEOUT_MS } from '@shared/constants'; import { useDismissOnAnyKey } from '@ui/tui/hooks/useDismissOnAnyKey'; interface SessionTimeoutScreenProps { diff --git a/src/ui/tui/screens/SkillSourceInfo.tsx b/src/ui/tui/screens/SkillSourceInfo.tsx index b5184b682..5edd8f443 100644 --- a/src/ui/tui/screens/SkillSourceInfo.tsx +++ b/src/ui/tui/screens/SkillSourceInfo.tsx @@ -15,7 +15,7 @@ import { Box, Text } from 'ink'; import { useEffect, useState } from 'react'; import { fetchSkillMenu, type SkillEntry } from '@lib/wizard-tools'; -import { CONTEXT_MILL_RELEASES_URL, getSkillsBaseUrl } from '@lib/constants'; +import { CONTEXT_MILL_RELEASES_URL, getSkillsBaseUrl } from '@shared/constants'; /** * Resolve a session skillId against the skill-menu entries. diff --git a/src/ui/tui/screens/SlackConnectScreen.tsx b/src/ui/tui/screens/SlackConnectScreen.tsx index f0f4d99c8..ecc1958ba 100644 --- a/src/ui/tui/screens/SlackConnectScreen.tsx +++ b/src/ui/tui/screens/SlackConnectScreen.tsx @@ -31,7 +31,7 @@ import { Colors, Icons } from '@ui/tui/styles'; import { PickerMenu, LoadingBox } from '@ui/tui/primitives/index'; import { useKeyBindings, KeyMatch } from '@ui/tui/hooks/useKeyBindings'; import { getSlackAppCard } from '@lib/mcp-role-prompts'; -import { fetchSlackConnected } from '@lib/api'; +import { fetchSlackConnected } from '@shared/api'; import { Program } from '@lib/programs/program-registry'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { analytics } from '@utils/analytics'; diff --git a/src/ui/tui/screens/doctor/DoctorReportScreen.tsx b/src/ui/tui/screens/doctor/DoctorReportScreen.tsx index eb4a982de..3e44e6d45 100644 --- a/src/ui/tui/screens/doctor/DoctorReportScreen.tsx +++ b/src/ui/tui/screens/doctor/DoctorReportScreen.tsx @@ -8,8 +8,8 @@ import { type HealthIssue, } from '@lib/programs/posthog-doctor/index'; import { OutroKind } from '@lib/wizard-session'; -import { ApiError } from '@lib/api'; -import { POSTHOG_DOCS_URL } from '@lib/constants'; +import { ApiError } from '@shared/api'; +import { POSTHOG_DOCS_URL } from '@shared/constants'; import { IssueTable, SEVERITY_LABEL, SEVERITY_ORDER } from './IssueTable.js'; interface DoctorReportScreenProps { diff --git a/src/ui/tui/screens/health/HealthCheckScreen.tsx b/src/ui/tui/screens/health/HealthCheckScreen.tsx index 8627e3ec7..4698cd178 100644 --- a/src/ui/tui/screens/health/HealthCheckScreen.tsx +++ b/src/ui/tui/screens/health/HealthCheckScreen.tsx @@ -20,12 +20,12 @@ import { ServiceHealthList } from '@ui/tui/components/ServiceHealthList'; import { getBlockingServiceKeys, SIGNUP_WIZARD_READINESS_CONFIG, -} from '@lib/health-checks/readiness'; -import { ServiceHealthStatus } from '@lib/health-checks/types'; +} from '@shared/health-checks/readiness'; +import { ServiceHealthStatus } from '@shared/health-checks/types'; import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; import { fetchSkillMenu, downloadSkill } from '@lib/wizard-tools'; -import { GITHUB_SKILLS_BASE_URL } from '@lib/constants'; +import { GITHUB_SKILLS_BASE_URL } from '@shared/constants'; import { useDismissOnAnyKey } from '@ui/tui/hooks/useDismissOnAnyKey'; interface HealthCheckScreenProps { diff --git a/src/ui/tui/services/mcp-suggested-prompts-services.ts b/src/ui/tui/services/mcp-suggested-prompts-services.ts index 484a882a8..5561a6da1 100644 --- a/src/ui/tui/services/mcp-suggested-prompts-services.ts +++ b/src/ui/tui/services/mcp-suggested-prompts-services.ts @@ -14,7 +14,7 @@ import type { Credentials } from '@lib/wizard-session'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { Program } from '@lib/programs/program-registry'; import type { WizardStore } from '@ui/tui/store'; -import type { ApiUser } from '@lib/api'; +import type { ApiUser } from '@shared/api'; import { probeProjectData as runProbe, type ProjectDataProfile, diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index f4214510e..8fc0888bd 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -40,7 +40,7 @@ import { WizardReadiness, getBlockingServiceKeys, type WizardReadinessResult, -} from '@lib/health-checks/readiness'; +} from '@shared/health-checks/readiness'; import { WizardRouter, type ScreenName, @@ -57,8 +57,8 @@ import type { import { getProgramConfig } from '@lib/programs/program-registry'; import { withAiOptInGate } from '@lib/programs/ai-opt-in-gate'; import { reportWarehouseSourcesDetected } from '@lib/programs/posthog-integration/detect'; -import { appendStatus } from '@lib/status-history'; -import { IS_DEV } from '@lib/constants'; +import { appendStatus } from '@shared/status-history'; +import { IS_DEV } from '@shared/constants'; import { computeTokenCostUsd } from '@lib/agent/token-pricing'; export { TaskStatus, ScreenId, Overlay, Program, RunPhase, McpOutcome }; diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index 17f2792f3..b35bba543 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -9,8 +9,8 @@ */ import type { SettingsConflict } from '@lib/agent/claude-settings'; -import type { WizardReadinessResult } from '@lib/health-checks/readiness'; -import type { ApiUser } from '@lib/api'; +import type { WizardReadinessResult } from '@shared/health-checks/readiness'; +import type { ApiUser } from '@shared/api'; import type { Credentials, TaskNotice } from '@lib/wizard-session'; import type { AskAnswers, diff --git a/src/wizard.ts b/src/wizard.ts index 621e14699..9b54de5f4 100644 --- a/src/wizard.ts +++ b/src/wizard.ts @@ -2,12 +2,12 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import type { Argv } from 'yargs'; import { IS_PRODUCTION_BUILD } from '@env'; -import { Harness, Sequence } from '@lib/constants'; +import { Harness, Sequence } from '@shared/constants'; import { regionOption } from '@lib/headless-mode'; -import { initLocalDev, localMcpSkillsNotice } from '@lib/local-dev'; +import { initLocalDev, localMcpSkillsNotice } from '@shared/local-dev'; import { toCommandModule, type Command } from './commands/command'; -import { ErrorCodes } from '@lib/errors'; -import { emitWizardError } from '@lib/errors'; +import { ErrorCodes } from '@shared/errors'; +import { emitWizardError } from '@shared/errors'; /** * Global yargs options applied to every command. These are read from the diff --git a/tsconfig.build.json b/tsconfig.build.json index 3457f5ffe..70420b6ae 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -19,6 +19,7 @@ "jsx": "react-jsx", "paths": { "@env": ["./src/env.ts"], + "@shared/*": ["./src/shared/*"], "@lib/*": ["./src/lib/*"], "@e2e-harness/*": ["./e2e-harness/*"], "@utils/*": ["./src/shared/utils/*"], diff --git a/vitest.config.ts b/vitest.config.ts index af7eabb86..9107d17e9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -85,6 +85,7 @@ export default defineConfig({ replacement: r('__mocks__/@posthog/warlock.ts'), }, { find: /^ink$/, replacement: r('__mocks__/ink.ts') }, + { find: /^@shared\/(.*)$/, replacement: `${r('src/shared')}/$1` }, // Path aliases — mirror tsconfig `paths`. { find: /^@env$/, replacement: r('src/env.ts') }, { find: /^@lib\/(.*)$/, replacement: `${r('src/lib')}/$1` }, From 0de7b0b127937dbe903d426836f982c86e2e9f88 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 00:19:12 -0400 Subject: [PATCH 22/61] refactor(shared): move settings pricing and phase helpers out of agent Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- bin.ts | 2 +- .../architecture/known-violations.json | 23 +++++-------------- .../credential-isolation-snapshot.test.ts | 4 ++-- .../__tests__/settings-conflicts.test.ts | 4 ++-- src/lib/agent/agent-interface.ts | 4 ++-- src/lib/agent/progress.ts | 2 +- src/lib/middleware/benchmarks/cost-tracker.ts | 2 +- .../__tests__/run-agent-legacy.test.ts | 2 +- src/lib/programs/run-agent-legacy.ts | 2 +- src/lib/wizard-session.ts | 2 +- .../__tests__/claude-settings-backup.test.ts | 2 +- .../__tests__/token-pricing.test.ts | 2 +- src/{lib/agent => shared}/agent-phase.ts | 0 src/{lib/agent => shared}/claude-settings.ts | 2 +- src/{lib/agent => shared}/token-pricing.ts | 0 src/ui/logging-ui.ts | 2 +- src/ui/tui/__tests__/store-invariants.test.ts | 2 +- src/ui/tui/components/PhaseVisuals.tsx | 2 +- src/ui/tui/components/TokenCostHud.tsx | 2 +- src/ui/tui/exit-line.ts | 2 +- src/ui/tui/ink-ui.ts | 2 +- src/ui/tui/playground/demos/RunScreenDemo.tsx | 2 +- src/ui/tui/screens/ManagedSettingsScreen.tsx | 2 +- src/ui/tui/store.ts | 4 ++-- src/ui/wizard-ui.ts | 2 +- 25 files changed, 32 insertions(+), 43 deletions(-) rename src/{lib/agent => shared}/__tests__/claude-settings-backup.test.ts (99%) rename src/{lib/agent => shared}/__tests__/token-pricing.test.ts (99%) rename src/{lib/agent => shared}/agent-phase.ts (100%) rename src/{lib/agent => shared}/claude-settings.ts (99%) rename src/{lib/agent => shared}/token-pricing.ts (100%) diff --git a/bin.ts b/bin.ts index 0c369d380..3b61c3c9f 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 '@shared/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/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 9727fe328..cb4038f84 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -3,7 +3,6 @@ "src/commands/factories/family-picker.tsx -> src/commands/command.ts", "src/env.ts -> src/lib/headless-mode.ts", "src/lib/detection/agentic.ts -> src/lib/agent/agent-interface.ts", - "src/shared/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", @@ -31,7 +30,6 @@ "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/run-agent-legacy.ts -> src/lib/agent/agent-interface.ts", - "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/claude-settings.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/index.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", "src/lib/programs/run-agent-legacy.ts -> src/lib/yara-hooks.ts", @@ -47,31 +45,22 @@ "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-ask-bridge.ts -> src/lib/agent/progress.ts", - "src/lib/wizard-session.ts -> src/lib/agent/claude-settings.ts", "src/lib/wizard-session.ts -> src/lib/agent/progress.ts", "src/lib/wizard-tools/handoff.ts -> src/lib/agent/progress.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/shared/claude-settings.ts -> src/lib/agent/agent-env-isolation.ts", + "src/shared/errors/agent-map.ts -> src/lib/agent/signals.ts", + "src/shared/utils/package-manager.ts -> src/telemetry.ts", + "src/shared/utils/setup-utils.ts -> src/telemetry.ts", + "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts", "src/steps/add-mcp-server-to-clients/index.ts -> src/telemetry.ts", "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", "src/steps/run-prettier.ts -> src/telemetry.ts", "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", "src/ui/agent-progress.ts -> src/lib/agent/progress.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/ui/wizard-ui.ts -> src/lib/agent/progress.ts", - "src/shared/utils/package-manager.ts -> src/telemetry.ts", - "src/shared/utils/setup-utils.ts -> src/telemetry.ts", - "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts" + "src/ui/wizard-ui.ts -> src/lib/agent/progress.ts" ] } diff --git a/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts b/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts index a17d5ad04..8145a6c06 100644 --- a/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts +++ b/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts @@ -14,8 +14,8 @@ */ import { sanitizeAgentSubprocessEnv } from '@lib/agent/agent-env-isolation'; -import { classifySettingsConflicts } from '@lib/agent/claude-settings'; -import type { SettingsConflict } from '@lib/agent/claude-settings'; +import { classifySettingsConflicts } from '@shared/claude-settings'; +import type { SettingsConflict } from '@shared/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/agent/__tests__/settings-conflicts.test.ts index 31f78f3e6..ba6f67a6a 100644 --- a/src/lib/agent/__tests__/settings-conflicts.test.ts +++ b/src/lib/agent/__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 '@shared/claude-settings'; +import type { SettingsConflict } from '@shared/claude-settings'; import { buildAuthErrorContext } from '@lib/agent/agent-interface'; const OVERRIDE = JSON.stringify({ apiKeyHelper: 'echo sk-x' }); diff --git a/src/lib/agent/agent-interface.ts b/src/lib/agent/agent-interface.ts index fa3fa8904..3f5f41240 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/lib/agent/agent-interface.ts @@ -47,7 +47,7 @@ import { import { createTriageLLMProvider } from './triage-provider'; import type { LLMProvider } from '@posthog/warlock'; import { assembleCommandments } from './runner/switchboard/commandments'; -import { classifyToolToStage } from './agent-phase'; +import { classifyToolToStage } from '@shared/agent-phase'; import type { PackageManagerDetector } from '@utils/package-manager'; import { AgentSignals, @@ -68,7 +68,7 @@ import { checkAllSettingsConflicts, type SettingsConflict, type SettingsConflictSource, -} from './claude-settings'; +} from '@shared/claude-settings'; import { detectStoredClaudeLogin, hasStoredClaudeLogin, diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index 780c9f039..fe4858faf 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -9,7 +9,7 @@ * every existing runner is unchanged. */ -import type { SettingsConflict } from './claude-settings'; +import type { SettingsConflict } from '@shared/claude-settings'; // ── What the agent hands back and asks with ───────────────────────── diff --git a/src/lib/middleware/benchmarks/cost-tracker.ts b/src/lib/middleware/benchmarks/cost-tracker.ts index 7e33ba0f4..ce89091a9 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 '@shared/token-pricing'; import type { TokenData } from './token-tracker'; import type { CacheData } from './cache-tracker'; diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index 221828e94..9af88da2b 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -58,7 +58,7 @@ vi.mock('@lib/programs/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), refreshAccessTokenIfNeeded: vi.fn().mockResolvedValue(undefined), })); -vi.mock('@lib/agent/claude-settings', () => ({ +vi.mock('@shared/claude-settings', () => ({ checkAllSettingsConflicts: vi.fn().mockReturnValue([]), restoreClaudeSettings: vi.fn(), })); diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index a3f0c6fe6..703a4e519 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -33,7 +33,7 @@ import { checkAllSettingsConflicts, classifySettingsConflicts, restoreClaudeSettings, -} from '@lib/agent/claude-settings'; +} from '@shared/claude-settings'; import { flushScanReport } from '@lib/yara-hooks'; import { evaluateWizardReadiness, diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index e90e82f7e..52959c8e9 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -21,7 +21,7 @@ import { } from '@shared/constants'; import type { FrameworkConfig } from './framework-config'; import type { WizardReadinessResult } from '@shared/health-checks/readiness'; -import type { SettingsConflict } from './agent/claude-settings'; +import type { SettingsConflict } from '@shared/claude-settings'; import type { ApiUser, ApiProject, Credentials } from '@shared/api'; import type { CloudRegion } from '@utils/types'; import { diff --git a/src/lib/agent/__tests__/claude-settings-backup.test.ts b/src/shared/__tests__/claude-settings-backup.test.ts similarity index 99% rename from src/lib/agent/__tests__/claude-settings-backup.test.ts rename to src/shared/__tests__/claude-settings-backup.test.ts index 225c9696e..5f5085f55 100644 --- a/src/lib/agent/__tests__/claude-settings-backup.test.ts +++ b/src/shared/__tests__/claude-settings-backup.test.ts @@ -18,7 +18,7 @@ import { backupAndFixClaudeSettings, restoreClaudeSettings, recoverOrphanedSettingsBackups, -} from '@lib/agent/claude-settings'; +} from '@shared/claude-settings'; const SETTINGS = 'settings.json'; const BACKUP = 'settings.json.wizard-backup'; diff --git a/src/lib/agent/__tests__/token-pricing.test.ts b/src/shared/__tests__/token-pricing.test.ts similarity index 99% rename from src/lib/agent/__tests__/token-pricing.test.ts rename to src/shared/__tests__/token-pricing.test.ts index 69bed5e38..bd9c4f10d 100644 --- a/src/lib/agent/__tests__/token-pricing.test.ts +++ b/src/shared/__tests__/token-pricing.test.ts @@ -3,7 +3,7 @@ import { pricePerMtokForModel, formatTokenCount, formatCostUsd, -} from '@lib/agent/token-pricing'; +} from '@shared/token-pricing'; describe('pricePerMtokForModel', () => { it('defaults to Sonnet pricing (DEFAULT_AGENT_MODEL) when no model is given', () => { diff --git a/src/lib/agent/agent-phase.ts b/src/shared/agent-phase.ts similarity index 100% rename from src/lib/agent/agent-phase.ts rename to src/shared/agent-phase.ts diff --git a/src/lib/agent/claude-settings.ts b/src/shared/claude-settings.ts similarity index 99% rename from src/lib/agent/claude-settings.ts rename to src/shared/claude-settings.ts index 43da0b418..6765dc43b 100644 --- a/src/lib/agent/claude-settings.ts +++ b/src/shared/claude-settings.ts @@ -15,7 +15,7 @@ import { registerCleanup } from '@utils/wizard-abort'; import { BLOCKED_AGENT_ENV_KEYS, BLOCKED_AGENT_ENV_PATTERNS, -} from './agent-env-isolation'; +} from '@lib/agent/agent-env-isolation'; /** The file names Claude Code accepts for project settings, in priority order. */ const SETTINGS_FILENAMES = ['settings.json', 'settings'] as const; diff --git a/src/lib/agent/token-pricing.ts b/src/shared/token-pricing.ts similarity index 100% rename from src/lib/agent/token-pricing.ts rename to src/shared/token-pricing.ts diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index 0d8693cdd..3ddb26b92 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 '@shared/claude-settings'; import type { ApiUser } from '@shared/api'; import { OAUTH_TIMEOUT_MS } from '@shared/constants'; import { diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts index 2cb4f5e06..a8939578b 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 '@shared/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 '@shared/claude-settings'; vi.mock('@utils/analytics.js', () => ({ analytics: { diff --git a/src/ui/tui/components/PhaseVisuals.tsx b/src/ui/tui/components/PhaseVisuals.tsx index 3265359c3..1dea3644b 100644 --- a/src/ui/tui/components/PhaseVisuals.tsx +++ b/src/ui/tui/components/PhaseVisuals.tsx @@ -12,7 +12,7 @@ import { Box, Text, measureElement, type DOMElement } from 'ink'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { useTick } from '@ui/tui/hooks/useTick'; -import { AgentPhase } from '@lib/agent/agent-phase'; +import { AgentPhase } from '@shared/agent-phase'; import { MATRIX_FADE } from './visualizer/panel'; import { VISUALIZER_PALETTE } from './visualizer/palette'; import { MatrixRain } from './visualizer/MatrixRain'; diff --git a/src/ui/tui/components/TokenCostHud.tsx b/src/ui/tui/components/TokenCostHud.tsx index 83fb1085d..14632b756 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 '@shared/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..5d2ad68d6 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 '@shared/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 f36f3cc60..1822b649c 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 '@shared/claude-settings'; import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { ApiUser } from '@shared/api'; import type { diff --git a/src/ui/tui/playground/demos/RunScreenDemo.tsx b/src/ui/tui/playground/demos/RunScreenDemo.tsx index 27cc7d193..d1e34c728 100644 --- a/src/ui/tui/playground/demos/RunScreenDemo.tsx +++ b/src/ui/tui/playground/demos/RunScreenDemo.tsx @@ -23,7 +23,7 @@ import { import { Box, Text, useInput } from 'ink'; import { WizardStore, TaskStatus } from '@ui/tui/store'; import { DiscoveredFeature } from '@lib/wizard-session'; -import { AgentPhase } from '@lib/agent/agent-phase'; +import { AgentPhase } from '@shared/agent-phase'; import { SplitView, ProgressList, diff --git a/src/ui/tui/screens/ManagedSettingsScreen.tsx b/src/ui/tui/screens/ManagedSettingsScreen.tsx index dcb184aa6..9708a2951 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 '@shared/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 8fc0888bd..1f29dfef6 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 '@shared/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 { appendStatus } from '@shared/status-history'; import { IS_DEV } from '@shared/constants'; -import { computeTokenCostUsd } from '@lib/agent/token-pricing'; +import { computeTokenCostUsd } from '@shared/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 b35bba543..9d00d469f 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 '@shared/claude-settings'; import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { ApiUser } from '@shared/api'; import type { Credentials, TaskNotice } from '@lib/wizard-session'; From 719737a05d1dcf36ef4a36904985fea723a8f2ec Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 00:22:27 -0400 Subject: [PATCH 23/61] refactor(agent): move agent execution into its own source tree Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .eslintrc.cjs | 31 ++++---- scripts/check-screens.tsx | 2 +- scripts/tui-host.no-jest.ts | 2 +- scripts/warlock-smoke-test.ts | 4 +- .../architecture/import-boundaries.test.ts | 19 ++--- .../architecture/known-violations.json | 70 +++++++++++-------- src/__tests__/programs-cli.test.ts | 6 +- src/__tests__/skill-cli.test.ts | 6 +- .../__snapshots__/commandments.test.ts.snap | 0 ...credential-isolation-snapshot.test.ts.snap | 0 .../__tests__/agent-env-isolation.test.ts | 2 +- .../__tests__/agent-interface.test.ts | 6 +- .../__tests__/agent-prompt-loader.test.ts | 2 +- .../agent/__tests__/agent-prompt.test.ts | 4 +- .../__tests__/agent-runner-ask.test.ts | 2 +- .../agent/__tests__/aio-capture.test.ts | 2 +- .../agent/__tests__/bash-fence-python.test.ts | 2 +- .../agent/__tests__/commandments.test.ts | 4 +- .../credential-isolation-snapshot.test.ts | 2 +- .../__tests__/gateway-session.test.ts | 4 +- .../agent/__tests__/output-signals.test.ts | 4 +- .../__tests__/progress-collector.test.ts | 0 .../__tests__/run-agent-standalone.test.ts | 34 +++++---- .../agent/__tests__/run-tags.test.ts | 2 +- .../__tests__/settings-conflicts.test.ts | 2 +- .../agent/__tests__/stored-login.test.ts | 0 .../agent/__tests__/triage-provider.test.ts | 4 +- .../agent/__tests__/tutorial-run-tags.test.ts | 4 +- .../agent/__tests__/variant-gating.test.ts | 2 +- .../__tests__/wizard-ask-bridge.test.ts | 2 +- .../__tests__/wizard-can-use-tool.test.ts | 2 +- .../__tests__/wizard-tools.test.ts | 2 +- .../__tests__/yara-flush-report.test.ts | 2 +- .../__tests__/yara-hooks.test.ts | 2 +- .../__tests__/yara-policy.test.ts | 0 src/{lib => }/agent/agent-env-isolation.ts | 0 src/{lib => }/agent/agent-interface.ts | 12 ++-- src/{lib => }/agent/agent-prompt-loader.ts | 2 +- src/{lib => }/agent/agent-prompt.ts | 0 src/{lib => }/agent/agent-runner.ts | 0 src/{lib => }/agent/aio-capture.ts | 0 src/{lib => }/agent/bash-fence.ts | 2 +- src/{lib => }/agent/commandments.ts | 0 src/{lib => agent}/gateway-session.ts | 0 src/{lib => }/agent/mcp-prompt-streaming.ts | 8 +-- .../__tests__/benchmark-emit.test.ts | 2 +- src/{lib => agent}/middleware/benchmark.ts | 4 +- .../middleware/benchmarks/cache-tracker.ts | 2 +- .../benchmarks/compaction-tracker.ts | 4 +- .../benchmarks/context-size-tracker.ts | 2 +- .../middleware/benchmarks/cost-tracker.ts | 2 +- .../middleware/benchmarks/duration-tracker.ts | 2 +- .../middleware/benchmarks/index.ts | 4 +- .../middleware/benchmarks/json-writer.ts | 8 +-- .../middleware/benchmarks/summary.ts | 6 +- .../middleware/benchmarks/token-tracker.ts | 2 +- .../middleware/benchmarks/turn-counter.ts | 2 +- src/{lib => agent}/middleware/config.ts | 2 +- src/{lib => agent}/middleware/index.ts | 0 .../middleware/phase-detector.ts | 0 src/{lib => agent}/middleware/pipeline.ts | 0 src/{lib => agent}/middleware/types.ts | 2 +- src/{lib => }/agent/output-signals.ts | 0 src/{lib => }/agent/progress.ts | 0 src/{lib => }/agent/runner/README.md | 0 .../runner/__tests__/switchboard.test.ts | 6 +- .../runner/harness/agents-platform/README.md | 0 .../agent/runner/harness/anthropic/README.md | 0 .../__tests__/pending-question.test.ts | 8 +-- .../agent/runner/harness/anthropic/index.ts | 8 +-- .../agent/runner/harness/pi/README.md | 0 .../harness/pi/__tests__/completion.test.ts | 2 +- .../harness/pi/__tests__/env-lockdown.test.ts | 0 .../harness/pi/__tests__/gateway.test.ts | 2 +- .../harness/pi/__tests__/security.test.ts | 0 .../harness/pi/__tests__/status-line.test.ts | 2 +- .../harness/pi/__tests__/task-tools.test.ts | 0 .../runner/harness/pi/__tests__/tools.test.ts | 4 +- .../agent/runner/harness/pi/completion.ts | 2 +- .../agent/runner/harness/pi/gateway.ts | 2 +- .../agent/runner/harness/pi/index.ts | 18 ++--- src/{lib => }/agent/runner/harness/pi/mcp.ts | 0 .../runner/harness/pi/orchestrator-tools.ts | 0 .../agent/runner/harness/pi/runtime-notes.ts | 0 .../agent/runner/harness/pi/security.ts | 6 +- .../agent/runner/harness/pi/subagent.ts | 0 src/{lib => }/agent/runner/harness/pi/task.ts | 14 ++-- .../agent/runner/harness/pi/tasks.ts | 2 +- .../agent/runner/harness/pi/tools.ts | 11 +-- src/{lib => }/agent/runner/harness/types.ts | 12 ++-- src/{lib => }/agent/runner/index.ts | 4 +- src/{lib => }/agent/runner/sequence/README.md | 0 src/{lib => }/agent/runner/sequence/linear.ts | 8 +-- .../__tests__/completed-seeded-types.test.ts | 4 +- .../orchestrator/__tests__/executor.test.ts | 4 +- .../__tests__/handoff-schema-parity.test.ts | 0 .../__tests__/optional-tasks.test.ts | 4 +- .../__tests__/queue-tools.test.ts | 11 ++- .../orchestrator/__tests__/queue.test.ts | 2 +- .../__tests__/run-metrics.test.ts | 2 +- .../__tests__/seeded-decline-skip.test.ts | 4 +- .../__tests__/seeded-deps.test.ts | 4 +- .../__tests__/sink-closure.test.ts | 8 +-- .../__tests__/skill-sweep.test.ts | 2 +- .../__tests__/task-notice-timeout.test.ts | 2 +- .../__tests__/variant-resolution.test.ts | 4 +- .../runner/sequence/orchestrator/executor.ts | 0 .../orchestrator/orchestrator-runner.ts | 10 +-- .../sequence/orchestrator/queue-tools.ts | 5 +- .../runner/sequence/orchestrator/queue.ts | 0 .../sequence/orchestrator/run-metrics.ts | 0 .../sequence/orchestrator/seeded-deps.ts | 0 src/{lib => }/agent/runner/shared/ask.ts | 4 +- .../agent/runner/shared/bootstrap.ts | 4 +- src/{lib => }/agent/runner/shared/errors.ts | 2 +- .../agent/runner/shared/progress-collector.ts | 2 +- src/{lib => }/agent/runner/shared/types.ts | 10 +-- .../agent/runner/switchboard/commandments.ts | 2 +- .../flags/__tests__/binding-cases.ts | 4 +- .../switchboard/flags/__tests__/flags.test.ts | 6 +- .../agent/runner/switchboard/flags/index.ts | 0 .../runner/switchboard/flags/orchestrator.ts | 0 .../agent/runner/switchboard/flags/schemes.ts | 0 .../runner/switchboard/flags/self-driving.ts | 0 .../agent/runner/switchboard/harness.ts | 0 .../agent/runner/switchboard/index.ts | 0 .../agent/runner/switchboard/models.ts | 0 .../agent/runner/switchboard/sequence.ts | 0 src/{lib => agent}/safe-tools.ts | 0 src/{lib => }/agent/signals.ts | 0 src/{lib => }/agent/stored-login.ts | 0 .../tools}/__tests__/handoff.test.ts | 2 +- .../wizard-tools => agent/tools}/handoff.ts | 2 +- .../wizard-tools => agent/tools}/index.ts | 0 src/{lib/wizard-tools => agent/tools}/mcp.ts | 4 +- .../wizard-tools => agent/tools}/tools.ts | 2 +- src/{lib => }/agent/triage-provider.ts | 4 +- src/{lib => agent}/wizard-ask-bridge.ts | 6 +- src/{lib => agent}/yara-hooks.ts | 0 src/{lib => agent}/yara-policy.ts | 0 .../factories/family-command-factory.ts | 2 +- src/commands/skill.ts | 2 +- src/lib/__tests__/wizard-spellbook.test.ts | 6 +- .../__tests__/agentic-progress.test.ts | 6 +- src/lib/detection/agentic.ts | 2 +- .../__tests__/run-agent-legacy.test.ts | 10 +-- .../__tests__/self-driving-detect.test.ts | 2 +- .../__tests__/self-driving-prompt.test.ts | 2 +- .../__tests__/warehouse-ask-timeout.test.ts | 2 +- .../web-analytics-doctor-detect.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 +- src/lib/programs/dispatch-family.ts | 4 +- .../detect.ts | 2 +- .../prompt.ts | 2 +- src/lib/programs/events-audit/index.ts | 2 +- src/lib/programs/mcp-analytics/index.ts | 2 +- src/lib/programs/migration/index.ts | 4 +- src/lib/programs/posthog-doctor/index.ts | 2 +- src/lib/programs/posthog-integration/index.ts | 6 +- src/lib/programs/program-run.ts | 2 +- src/lib/programs/replay-vision/index.ts | 2 +- src/lib/programs/revenue-analytics/detect.ts | 2 +- src/lib/programs/revenue-analytics/index.ts | 2 +- src/lib/programs/run-agent-legacy.ts | 8 +-- src/lib/programs/self-driving/detect.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 | 2 +- src/lib/wizard-session.ts | 2 +- src/lib/wizard-spellbook.ts | 2 +- src/shared/claude-settings.ts | 2 +- .../errors/__tests__/run-failure.test.ts | 2 +- src/shared/errors/agent-map.ts | 2 +- src/shared/errors/skill-map.ts | 2 +- .../utils/__tests__/environment.test.ts | 2 +- src/ui/__tests__/agent-progress.test.ts | 2 +- src/ui/agent-progress.ts | 2 +- src/ui/tui/__tests__/frames.test.tsx | 2 +- .../__tests__/keyboard-equivalence.test.tsx | 4 +- src/ui/tui/__tests__/skill-entry.test.ts | 2 +- src/ui/tui/screens/SkillSourceInfo.tsx | 2 +- .../tui/screens/health/HealthCheckScreen.tsx | 2 +- .../mcp-suggested-prompts-services.ts | 6 +- src/ui/wizard-ui.ts | 2 +- tsconfig.build.json | 1 + tsconfig.json | 1 - vitest.config.ts | 10 +-- 191 files changed, 328 insertions(+), 349 deletions(-) rename src/{lib => }/agent/__tests__/__snapshots__/commandments.test.ts.snap (100%) 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__/agent-interface.test.ts (99%) rename src/{lib => }/agent/__tests__/agent-prompt-loader.test.ts (99%) rename src/{lib => }/agent/__tests__/agent-prompt.test.ts (93%) rename src/{lib => agent}/__tests__/agent-runner-ask.test.ts (97%) rename src/{lib => }/agent/__tests__/aio-capture.test.ts (99%) rename src/{lib => }/agent/__tests__/bash-fence-python.test.ts (98%) rename src/{lib => }/agent/__tests__/commandments.test.ts (97%) rename src/{lib => }/agent/__tests__/credential-isolation-snapshot.test.ts (98%) rename src/{lib => agent}/__tests__/gateway-session.test.ts (99%) rename src/{lib => }/agent/__tests__/output-signals.test.ts (97%) rename src/{lib => }/agent/__tests__/progress-collector.test.ts (100%) rename src/{lib => }/agent/__tests__/run-agent-standalone.test.ts (95%) rename src/{lib => }/agent/__tests__/run-tags.test.ts (97%) rename src/{lib => }/agent/__tests__/settings-conflicts.test.ts (99%) rename src/{lib => }/agent/__tests__/stored-login.test.ts (100%) rename src/{lib => }/agent/__tests__/triage-provider.test.ts (97%) rename src/{lib => }/agent/__tests__/tutorial-run-tags.test.ts (94%) rename src/{lib => }/agent/__tests__/variant-gating.test.ts (89%) rename src/{lib => agent}/__tests__/wizard-ask-bridge.test.ts (99%) rename src/{lib => agent}/__tests__/wizard-can-use-tool.test.ts (99%) rename src/{lib => agent}/__tests__/wizard-tools.test.ts (99%) rename src/{lib => agent}/__tests__/yara-flush-report.test.ts (98%) rename src/{lib => agent}/__tests__/yara-hooks.test.ts (99%) rename src/{lib => agent}/__tests__/yara-policy.test.ts (100%) rename src/{lib => }/agent/agent-env-isolation.ts (100%) rename src/{lib => }/agent/agent-interface.ts (99%) rename src/{lib => }/agent/agent-prompt-loader.ts (99%) rename src/{lib => }/agent/agent-prompt.ts (100%) rename src/{lib => }/agent/agent-runner.ts (100%) rename src/{lib => }/agent/aio-capture.ts (100%) rename src/{lib => }/agent/bash-fence.ts (99%) rename src/{lib => }/agent/commandments.ts (100%) rename src/{lib => agent}/gateway-session.ts (100%) rename src/{lib => }/agent/mcp-prompt-streaming.ts (98%) rename src/{lib => agent}/middleware/__tests__/benchmark-emit.test.ts (98%) rename src/{lib => agent}/middleware/benchmark.ts (95%) rename src/{lib => agent}/middleware/benchmarks/cache-tracker.ts (99%) rename src/{lib => agent}/middleware/benchmarks/compaction-tracker.ts (96%) rename src/{lib => agent}/middleware/benchmarks/context-size-tracker.ts (98%) rename src/{lib => agent}/middleware/benchmarks/cost-tracker.ts (99%) rename src/{lib => agent}/middleware/benchmarks/duration-tracker.ts (97%) rename src/{lib => agent}/middleware/benchmarks/index.ts (95%) rename src/{lib => agent}/middleware/benchmarks/json-writer.ts (96%) rename src/{lib => agent}/middleware/benchmarks/summary.ts (96%) rename src/{lib => agent}/middleware/benchmarks/token-tracker.ts (98%) rename src/{lib => agent}/middleware/benchmarks/turn-counter.ts (98%) rename src/{lib => agent}/middleware/config.ts (98%) rename src/{lib => agent}/middleware/index.ts (100%) rename src/{lib => agent}/middleware/phase-detector.ts (100%) rename src/{lib => agent}/middleware/pipeline.ts (100%) rename src/{lib => agent}/middleware/types.ts (96%) rename src/{lib => }/agent/output-signals.ts (100%) rename src/{lib => }/agent/progress.ts (100%) rename src/{lib => }/agent/runner/README.md (100%) rename src/{lib => }/agent/runner/__tests__/switchboard.test.ts (98%) rename src/{lib => }/agent/runner/harness/agents-platform/README.md (100%) rename src/{lib => }/agent/runner/harness/anthropic/README.md (100%) rename src/{lib => }/agent/runner/harness/anthropic/__tests__/pending-question.test.ts (95%) rename src/{lib => }/agent/runner/harness/anthropic/index.ts (95%) rename src/{lib => }/agent/runner/harness/pi/README.md (100%) rename src/{lib => }/agent/runner/harness/pi/__tests__/completion.test.ts (97%) rename src/{lib => }/agent/runner/harness/pi/__tests__/env-lockdown.test.ts (100%) rename src/{lib => }/agent/runner/harness/pi/__tests__/gateway.test.ts (99%) rename src/{lib => }/agent/runner/harness/pi/__tests__/security.test.ts (100%) rename src/{lib => }/agent/runner/harness/pi/__tests__/status-line.test.ts (96%) rename src/{lib => }/agent/runner/harness/pi/__tests__/task-tools.test.ts (100%) rename src/{lib => }/agent/runner/harness/pi/__tests__/tools.test.ts (99%) rename src/{lib => }/agent/runner/harness/pi/completion.ts (95%) rename src/{lib => }/agent/runner/harness/pi/gateway.ts (99%) rename src/{lib => }/agent/runner/harness/pi/index.ts (97%) rename src/{lib => }/agent/runner/harness/pi/mcp.ts (100%) rename src/{lib => }/agent/runner/harness/pi/orchestrator-tools.ts (100%) rename src/{lib => }/agent/runner/harness/pi/runtime-notes.ts (100%) rename src/{lib => }/agent/runner/harness/pi/security.ts (99%) rename src/{lib => }/agent/runner/harness/pi/subagent.ts (100%) rename src/{lib => }/agent/runner/harness/pi/task.ts (97%) rename src/{lib => }/agent/runner/harness/pi/tasks.ts (98%) rename src/{lib => }/agent/runner/harness/pi/tools.ts (99%) rename src/{lib => }/agent/runner/harness/types.ts (93%) rename src/{lib => }/agent/runner/index.ts (98%) rename src/{lib => }/agent/runner/sequence/README.md (100%) rename src/{lib => }/agent/runner/sequence/linear.ts (97%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts (93%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/executor.test.ts (98%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/handoff-schema-parity.test.ts (100%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts (90%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts (97%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/queue.test.ts (99%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts (97%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts (96%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts (99%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts (98%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts (98%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts (99%) rename src/{lib => }/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts (98%) rename src/{lib => }/agent/runner/sequence/orchestrator/executor.ts (100%) rename src/{lib => }/agent/runner/sequence/orchestrator/orchestrator-runner.ts (99%) rename src/{lib => }/agent/runner/sequence/orchestrator/queue-tools.ts (99%) rename src/{lib => }/agent/runner/sequence/orchestrator/queue.ts (100%) rename src/{lib => }/agent/runner/sequence/orchestrator/run-metrics.ts (100%) rename src/{lib => }/agent/runner/sequence/orchestrator/seeded-deps.ts (100%) rename src/{lib => }/agent/runner/shared/ask.ts (92%) rename src/{lib => }/agent/runner/shared/bootstrap.ts (97%) rename src/{lib => }/agent/runner/shared/errors.ts (95%) rename src/{lib => }/agent/runner/shared/progress-collector.ts (99%) rename src/{lib => }/agent/runner/shared/types.ts (97%) rename src/{lib => }/agent/runner/switchboard/commandments.ts (99%) rename src/{lib => }/agent/runner/switchboard/flags/__tests__/binding-cases.ts (94%) rename src/{lib => }/agent/runner/switchboard/flags/__tests__/flags.test.ts (98%) rename src/{lib => }/agent/runner/switchboard/flags/index.ts (100%) rename src/{lib => }/agent/runner/switchboard/flags/orchestrator.ts (100%) rename src/{lib => }/agent/runner/switchboard/flags/schemes.ts (100%) rename src/{lib => }/agent/runner/switchboard/flags/self-driving.ts (100%) rename src/{lib => }/agent/runner/switchboard/harness.ts (100%) rename src/{lib => }/agent/runner/switchboard/index.ts (100%) rename src/{lib => }/agent/runner/switchboard/models.ts (100%) rename src/{lib => }/agent/runner/switchboard/sequence.ts (100%) rename src/{lib => agent}/safe-tools.ts (100%) rename src/{lib => }/agent/signals.ts (100%) rename src/{lib => }/agent/stored-login.ts (100%) rename src/{lib/wizard-tools => agent/tools}/__tests__/handoff.test.ts (99%) rename src/{lib/wizard-tools => agent/tools}/handoff.ts (99%) rename src/{lib/wizard-tools => agent/tools}/index.ts (100%) rename src/{lib/wizard-tools => agent/tools}/mcp.ts (99%) rename src/{lib/wizard-tools => agent/tools}/tools.ts (99%) rename src/{lib => }/agent/triage-provider.ts (97%) rename src/{lib => agent}/wizard-ask-bridge.ts (98%) rename src/{lib => agent}/yara-hooks.ts (100%) rename src/{lib => agent}/yara-policy.ts (100%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e3fa6cb1d..849d0ed71 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -35,18 +35,8 @@ module.exports = { // progress events and asks through an injected answerer, so nothing // here may reach a UI, the session, detection, the CLI or a program at // runtime. Program types stay importable until B1 moves - // PROGRAM_BINDINGS to programs. Today's paths; A2b collapses them to - // src/agent/**. - files: [ - 'src/lib/agent/**/*.ts', - 'src/lib/middleware/**/*.ts', - 'src/lib/wizard-tools/**/*.ts', - 'src/lib/gateway-session.ts', - 'src/lib/safe-tools.ts', - 'src/lib/wizard-ask-bridge.ts', - 'src/lib/yara-hooks.ts', - 'src/lib/yara-policy.ts', - ], + // PROGRAM_BINDINGS to programs. + files: ['src/agent/**/*.ts'], excludedFiles: ['**/__tests__/**'], rules: { '@typescript-eslint/no-restricted-imports': [ @@ -59,6 +49,12 @@ module.exports = { message: 'The agent never exits the process: return a failure in RunResult.', }, + { + name: '@shared/utils/wizard-abort', + importNames: ['wizardAbort'], + message: + 'The agent never exits the process: return a failure in RunResult.', + }, ], patterns: [ { @@ -66,13 +62,12 @@ module.exports = { '@ui', '@ui/**', '**/ui/**', - '@lib/wizard-session', + '@lib/**', + '!@lib/programs/**', + '**/lib/**', + '!**/lib/programs/**', '**/wizard-session', - '@lib/detection/**', '**/detection/**', - '@lib/registry', - '**/lib/registry', - '@lib/runners/**', '**/runners/**', '**/commands/**', '@steps', @@ -80,8 +75,10 @@ module.exports = { '@frameworks/**', '**/frameworks/**', '@utils/setup-utils', + '@shared/utils/setup-utils', '**/setup-utils', '@utils/oauth', + '@shared/utils/oauth', '**/utils/oauth', ], message: diff --git a/scripts/check-screens.tsx b/scripts/check-screens.tsx index bfc2871b5..034106c70 100644 --- a/scripts/check-screens.tsx +++ b/scripts/check-screens.tsx @@ -15,7 +15,7 @@ import { ProgressList } from '@ui/tui/primitives/ProgressList'; import { ManagedSettingsScreen } from '@ui/tui/screens/ManagedSettingsScreen'; import { SettingsOverrideScreen } from '@ui/tui/screens/SettingsOverrideScreen'; import { WizardAskScreen } from '@ui/tui/screens/WizardAskScreen'; -import type { SettingsConflict } from '@lib/agent/agent-interface'; +import type { SettingsConflict } from '@agent/agent-interface'; function fakeStore(session: Record): any { return { diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 7dbcb690b..75b246b8e 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -26,7 +26,7 @@ import { import type { Harness, Sequence } from '@shared/constants'; import { buildSession } from '@lib/wizard-session'; import { initLocalDev } from '@shared/local-dev'; -import { configureGatewayFromCIEnvironment } from '@lib/gateway-session'; +import { configureGatewayFromCIEnvironment } from '@agent/gateway-session'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; import { getAuditChecks } from '@lib/programs/audit/types'; diff --git a/scripts/warlock-smoke-test.ts b/scripts/warlock-smoke-test.ts index 089e4110d..ad261bc5e 100644 --- a/scripts/warlock-smoke-test.ts +++ b/scripts/warlock-smoke-test.ts @@ -11,8 +11,8 @@ import { scan, CATEGORIES, type Category } from '@posthog/warlock'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { scanVerdict } from '../src/lib/yara-policy'; -import { scanInstalledSkill } from '../src/lib/yara-hooks'; +import { scanVerdict } from '@agent/yara-policy'; +import { scanInstalledSkill } from '@agent/yara-hooks'; // Assembled at runtime so no scannable secret literal is committed. const fakeStripeKey = 'sk_' + 'live_' + '5'.repeat(24); diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index 8220182ae..dba4024a4 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -10,16 +10,7 @@ const REPO_ROOT = path.resolve(HERE, '../../..'); const SURFACE_RULES: ReadonlyArray boolean]> = [ ['env', (p) => p === 'src/env.ts'], - [ - 'agent', - (p) => - p.startsWith('src/lib/agent/') || - p.startsWith('src/lib/middleware/') || - p === 'src/lib/gateway-session.ts' || - p === 'src/lib/yara-hooks.ts' || - p === 'src/lib/yara-policy.ts' || - p === 'src/lib/wizard-tools/mcp.ts', - ], + ['agent', (p) => p.startsWith('src/agent/')], [ 'tui', (p) => @@ -384,12 +375,12 @@ describe('import boundaries', () => { describe('surface classification', () => { it('maps representative paths to their surface', () => { expect(classifySurface('src/env.ts')).toBe('env'); - expect(classifySurface('src/utils/analytics.ts')).toBe('legacy'); - expect(classifySurface('src/lib/agent/agent-runner.ts')).toBe('agent'); + expect(classifySurface('src/shared/utils/analytics.ts')).toBe('legacy'); + expect(classifySurface('src/agent/agent-runner.ts')).toBe('agent'); expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); expect(classifySurface('bin.ts')).toBe('cli'); - expect(classifySurface('src/lib/wizard-tools/mcp.ts')).toBe('agent'); - expect(classifySurface('src/lib/wizard-tools/tools.ts')).toBe('legacy'); + expect(classifySurface('src/agent/tools/mcp.ts')).toBe('agent'); + expect(classifySurface('src/agent/tools/tools.ts')).toBe('agent'); expect(classifySurface('src/commands/factories/family-picker.tsx')).toBe( 'tui', ); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index cb4038f84..1f0f7bd21 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -2,55 +2,61 @@ "violations": [ "src/commands/factories/family-picker.tsx -> src/commands/command.ts", "src/env.ts -> src/lib/headless-mode.ts", - "src/lib/detection/agentic.ts -> src/lib/agent/agent-interface.ts", - "src/lib/programs/agent-skill/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/detection/agentic.ts -> src/agent/agent-interface.ts", + "src/lib/programs/agent-skill/index.ts -> src/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/detect.ts -> src/agent/agent-runner.ts", + "src/lib/programs/audit/index.ts -> src/agent/tools/index.ts", + "src/lib/programs/dispatch-family.ts -> src/agent/tools/index.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/detect.ts -> src/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-upload-source-maps/prompt.ts -> src/agent/agent-interface.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/mcp-analytics/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/events-audit/index.ts -> src/agent/tools/index.ts", + "src/lib/programs/mcp-analytics/index.ts -> src/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/agent/agent-runner.ts", + "src/lib/programs/migration/index.ts -> src/agent/tools/index.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-doctor/index.ts -> src/agent/tools/index.ts", + "src/lib/programs/posthog-integration/index.ts -> src/agent/agent-interface.ts", + "src/lib/programs/posthog-integration/index.ts -> src/agent/agent-runner.ts", + "src/lib/programs/posthog-integration/index.ts -> src/agent/tools/index.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-run.ts -> src/lib/agent/runner/index.ts", + "src/lib/programs/program-run.ts -> src/agent/runner/index.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/replay-vision/index.ts -> src/agent/agent-runner.ts", + "src/lib/programs/revenue-analytics/detect.ts -> src/agent/agent-runner.ts", + "src/lib/programs/revenue-analytics/index.ts -> src/agent/tools/index.ts", "src/lib/programs/revenue-analytics/index.ts -> src/lib/programs/revenue-analytics/content/index.tsx", - "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/agent-interface.ts", - "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/index.ts", - "src/lib/programs/run-agent-legacy.ts -> src/lib/agent/runner/switchboard/index.ts", - "src/lib/programs/run-agent-legacy.ts -> src/lib/yara-hooks.ts", - "src/lib/programs/self-driving/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/run-agent-legacy.ts -> src/agent/agent-interface.ts", + "src/lib/programs/run-agent-legacy.ts -> src/agent/runner/index.ts", + "src/lib/programs/run-agent-legacy.ts -> src/agent/runner/switchboard/index.ts", + "src/lib/programs/run-agent-legacy.ts -> src/agent/yara-hooks.ts", + "src/lib/programs/self-driving/detect.ts -> src/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/self-driving/prompt.ts -> src/agent/agent-interface.ts", + "src/lib/programs/self-driving/prompt.ts -> src/agent/agent-runner.ts", + "src/lib/programs/warehouse-source/detect.ts -> src/agent/agent-runner.ts", + "src/lib/programs/warehouse-source/index.ts -> src/agent/wizard-ask-bridge.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/programs/web-analytics-doctor/detect.ts -> src/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-ask-bridge.ts -> src/lib/agent/progress.ts", - "src/lib/wizard-session.ts -> src/lib/agent/progress.ts", - "src/lib/wizard-tools/handoff.ts -> src/lib/agent/progress.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/shared/claude-settings.ts -> src/lib/agent/agent-env-isolation.ts", - "src/shared/errors/agent-map.ts -> src/lib/agent/signals.ts", + "src/lib/wizard-session.ts -> src/agent/progress.ts", + "src/lib/wizard-spellbook.ts -> src/agent/tools/tools.ts", + "src/shared/claude-settings.ts -> src/agent/agent-env-isolation.ts", + "src/shared/errors/agent-map.ts -> src/agent/signals.ts", + "src/shared/errors/skill-map.ts -> src/agent/tools/index.ts", "src/shared/utils/package-manager.ts -> src/telemetry.ts", "src/shared/utils/setup-utils.ts -> src/telemetry.ts", "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts", @@ -58,9 +64,11 @@ "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", "src/steps/run-prettier.ts -> src/telemetry.ts", "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", - "src/ui/agent-progress.ts -> src/lib/agent/progress.ts", + "src/ui/agent-progress.ts -> src/agent/progress.ts", "src/ui/index.ts -> src/ui/logging-ui.ts", - "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/lib/agent/mcp-prompt-streaming.ts", - "src/ui/wizard-ui.ts -> src/lib/agent/progress.ts" + "src/ui/tui/screens/SkillSourceInfo.tsx -> src/agent/tools/index.ts", + "src/ui/tui/screens/health/HealthCheckScreen.tsx -> src/agent/tools/index.ts", + "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/mcp-prompt-streaming.ts", + "src/ui/wizard-ui.ts -> src/agent/progress.ts" ] } diff --git a/src/__tests__/programs-cli.test.ts b/src/__tests__/programs-cli.test.ts index 89a972809..9f24ddd10 100644 --- a/src/__tests__/programs-cli.test.ts +++ b/src/__tests__/programs-cli.test.ts @@ -8,8 +8,8 @@ vi.mock('@lib/runners', () => ({ runWizardCI: mockRunWizardCI, })); -vi.mock('@lib/wizard-tools', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent/tools', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, fetchSkillMenu: vi.fn(), @@ -31,7 +31,7 @@ import { pickerChildrenToShow, } from '@lib/programs/dispatch-family'; import type { Command } from '../commands/command'; -import { fetchSkillMenu, type CliEntry } from '@lib/wizard-tools'; +import { fetchSkillMenu, type CliEntry } from '@agent/tools'; import { auditConfig } from '@lib/programs/audit/index'; import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; import { parseCommand } from './helpers/parse-command.no-jest'; diff --git a/src/__tests__/skill-cli.test.ts b/src/__tests__/skill-cli.test.ts index 7acc361ab..aed48a047 100644 --- a/src/__tests__/skill-cli.test.ts +++ b/src/__tests__/skill-cli.test.ts @@ -4,13 +4,13 @@ vi.mock('../commands/basic-integration/skill', () => ({ runSkillMode: vi.fn(), })); -vi.mock('@lib/wizard-tools', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent/tools', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, fetchSkillMenu: vi.fn() }; }); import { runSkillMode } from '../commands/basic-integration/skill'; -import { fetchSkillMenu } from '@lib/wizard-tools'; +import { fetchSkillMenu } from '@agent/tools'; import { analytics } from '@utils/analytics'; import { skillCommand } from '../commands/skill'; import { parseCommand } from './helpers/parse-command.no-jest'; diff --git a/src/lib/agent/__tests__/__snapshots__/commandments.test.ts.snap b/src/agent/__tests__/__snapshots__/commandments.test.ts.snap similarity index 100% rename from src/lib/agent/__tests__/__snapshots__/commandments.test.ts.snap rename to src/agent/__tests__/__snapshots__/commandments.test.ts.snap diff --git a/src/lib/agent/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap b/src/agent/__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/agent/__tests__/__snapshots__/credential-isolation-snapshot.test.ts.snap diff --git a/src/lib/agent/__tests__/agent-env-isolation.test.ts b/src/agent/__tests__/agent-env-isolation.test.ts similarity index 99% rename from src/lib/agent/__tests__/agent-env-isolation.test.ts rename to src/agent/__tests__/agent-env-isolation.test.ts index 29a1feaf3..3a8c9c6d6 100644 --- a/src/lib/agent/__tests__/agent-env-isolation.test.ts +++ b/src/agent/__tests__/agent-env-isolation.test.ts @@ -2,7 +2,7 @@ import { sanitizeAgentSubprocessEnv, isBlockedAgentEnvKey, BLOCKED_AGENT_ENV_KEYS, -} from '@lib/agent/agent-env-isolation'; +} from '@agent/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/agent/__tests__/agent-interface.test.ts similarity index 99% rename from src/lib/__tests__/agent-interface.test.ts rename to src/agent/__tests__/agent-interface.test.ts index 8c40c8618..a1bec56b2 100644 --- a/src/lib/__tests__/agent-interface.test.ts +++ b/src/agent/__tests__/agent-interface.test.ts @@ -8,9 +8,9 @@ import { buildAuthErrorContext, buildAgentEnv, reportMcpSetup, -} from '@lib/agent/agent-interface'; -import { AgentOutputSignals } from '@lib/agent/output-signals'; -import { RESUME_INSTRUCTION } from '@lib/agent/signals'; +} from '@agent/agent-interface'; +import { AgentOutputSignals } from '@agent/output-signals'; +import { RESUME_INSTRUCTION } from '@agent/signals'; import { analytics } from '@utils/analytics'; import { Sequence } from '@shared/constants'; import type { WizardRunOptions } from '@utils/types'; diff --git a/src/lib/agent/__tests__/agent-prompt-loader.test.ts b/src/agent/__tests__/agent-prompt-loader.test.ts similarity index 99% rename from src/lib/agent/__tests__/agent-prompt-loader.test.ts rename to src/agent/__tests__/agent-prompt-loader.test.ts index b04c7a925..a620a47dd 100644 --- a/src/lib/agent/__tests__/agent-prompt-loader.test.ts +++ b/src/agent/__tests__/agent-prompt-loader.test.ts @@ -17,7 +17,7 @@ import { type AgentRegistry, type OrchestratorPromptContext, } from '../agent-prompt-loader'; -import { QueueStore } from '@lib/agent/runner/sequence/orchestrator/queue'; +import { QueueStore } from '@agent/runner/sequence/orchestrator/queue'; import { HostResolution } from '@shared/host-resolution'; function tmpDir(): string { diff --git a/src/lib/agent/__tests__/agent-prompt.test.ts b/src/agent/__tests__/agent-prompt.test.ts similarity index 93% rename from src/lib/agent/__tests__/agent-prompt.test.ts rename to src/agent/__tests__/agent-prompt.test.ts index 302525c83..57d349cd5 100644 --- a/src/lib/agent/__tests__/agent-prompt.test.ts +++ b/src/agent/__tests__/agent-prompt.test.ts @@ -1,5 +1,5 @@ -import { assemblePrompt, type PromptContext } from '@lib/agent/agent-prompt'; -import type { AgentRunDefinition } from '@lib/agent/runner'; +import { assemblePrompt, type PromptContext } from '@agent/agent-prompt'; +import type { AgentRunDefinition } from '@agent/runner'; import { HostResolution } from '@shared/host-resolution'; function makeRunDef( diff --git a/src/lib/__tests__/agent-runner-ask.test.ts b/src/agent/__tests__/agent-runner-ask.test.ts similarity index 97% rename from src/lib/__tests__/agent-runner-ask.test.ts rename to src/agent/__tests__/agent-runner-ask.test.ts index 2364b5ccf..dfee8067f 100644 --- a/src/lib/__tests__/agent-runner-ask.test.ts +++ b/src/agent/__tests__/agent-runner-ask.test.ts @@ -1,4 +1,4 @@ -import { shouldDisableAsk } from '@lib/agent/agent-runner'; +import { shouldDisableAsk } from '@agent/agent-runner'; import { buildSession } from '@lib/wizard-session'; describe('shouldDisableAsk', () => { diff --git a/src/lib/agent/__tests__/aio-capture.test.ts b/src/agent/__tests__/aio-capture.test.ts similarity index 99% rename from src/lib/agent/__tests__/aio-capture.test.ts rename to src/agent/__tests__/aio-capture.test.ts index 042f1e2d3..5d0c8d90b 100644 --- a/src/lib/agent/__tests__/aio-capture.test.ts +++ b/src/agent/__tests__/aio-capture.test.ts @@ -1,4 +1,4 @@ -import { createAioCapture } from '@lib/agent/aio-capture'; +import { createAioCapture } from '@agent/aio-capture'; const RUN_TAGS = { program_id: 'posthog-integration', diff --git a/src/lib/agent/__tests__/bash-fence-python.test.ts b/src/agent/__tests__/bash-fence-python.test.ts similarity index 98% rename from src/lib/agent/__tests__/bash-fence-python.test.ts rename to src/agent/__tests__/bash-fence-python.test.ts index 1a45587a4..bb96f83ab 100644 --- a/src/lib/agent/__tests__/bash-fence-python.test.ts +++ b/src/agent/__tests__/bash-fence-python.test.ts @@ -1,4 +1,4 @@ -import { evaluateBashCommand } from '@lib/agent/bash-fence'; +import { evaluateBashCommand } from '@agent/bash-fence'; /** * The fence and the pi runtime notes have to agree: the notes tell the agent to diff --git a/src/lib/agent/__tests__/commandments.test.ts b/src/agent/__tests__/commandments.test.ts similarity index 97% rename from src/lib/agent/__tests__/commandments.test.ts rename to src/agent/__tests__/commandments.test.ts index 1a0dc6ffe..2a694c0f2 100644 --- a/src/lib/agent/__tests__/commandments.test.ts +++ b/src/agent/__tests__/commandments.test.ts @@ -1,5 +1,5 @@ -import { WIZARD_COMMANDMENTS } from '@lib/agent/commandments'; -import { assembleCommandments } from '@lib/agent/runner/switchboard/commandments'; +import { WIZARD_COMMANDMENTS } from '@agent/commandments'; +import { assembleCommandments } from '@agent/runner/switchboard/commandments'; import { Harness, Sequence } from '@shared/constants'; const global = WIZARD_COMMANDMENTS.join('\n'); diff --git a/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts b/src/agent/__tests__/credential-isolation-snapshot.test.ts similarity index 98% rename from src/lib/agent/__tests__/credential-isolation-snapshot.test.ts rename to src/agent/__tests__/credential-isolation-snapshot.test.ts index 8145a6c06..40649ebcb 100644 --- a/src/lib/agent/__tests__/credential-isolation-snapshot.test.ts +++ b/src/agent/__tests__/credential-isolation-snapshot.test.ts @@ -13,7 +13,7 @@ * isolation surface — a diff here means a credential path changed disposition. */ -import { sanitizeAgentSubprocessEnv } from '@lib/agent/agent-env-isolation'; +import { sanitizeAgentSubprocessEnv } from '@agent/agent-env-isolation'; import { classifySettingsConflicts } from '@shared/claude-settings'; import type { SettingsConflict } from '@shared/claude-settings'; diff --git a/src/lib/__tests__/gateway-session.test.ts b/src/agent/__tests__/gateway-session.test.ts similarity index 99% rename from src/lib/__tests__/gateway-session.test.ts rename to src/agent/__tests__/gateway-session.test.ts index 315ec407d..23d5562d8 100644 --- a/src/lib/__tests__/gateway-session.test.ts +++ b/src/agent/__tests__/gateway-session.test.ts @@ -9,7 +9,7 @@ import { isPastRefresh, isTrustedGatewayUrl, resetGatewaySession, -} from '@lib/gateway-session'; +} from '@agent/gateway-session'; import type { HostResolution } from '@shared/host-resolution'; import { ErrorCodes } from '@shared/errors'; import { WizardError } from '@utils/wizard-abort'; @@ -120,7 +120,7 @@ describe('gatewayAuth', () => { vi.stubEnv('NODE_ENV', 'production'); vi.resetModules(); try { - const prod = await import('@lib/gateway-session'); + const prod = await import('@agent/gateway-session'); expect(() => prod.configureGatewayCredentialsForCI( 'token', diff --git a/src/lib/agent/__tests__/output-signals.test.ts b/src/agent/__tests__/output-signals.test.ts similarity index 97% rename from src/lib/agent/__tests__/output-signals.test.ts rename to src/agent/__tests__/output-signals.test.ts index 76eeaef6c..2c2431935 100644 --- a/src/lib/agent/__tests__/output-signals.test.ts +++ b/src/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 { AgentOutputSignals } from '@agent/output-signals'; +import { AgentSignals, REMARK_INSTRUCTION } from '@agent/signals'; describe('REMARK_INSTRUCTION', () => { it('carries the marker but no literal placeholder a model could echo', () => { diff --git a/src/lib/agent/__tests__/progress-collector.test.ts b/src/agent/__tests__/progress-collector.test.ts similarity index 100% rename from src/lib/agent/__tests__/progress-collector.test.ts rename to src/agent/__tests__/progress-collector.test.ts diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts similarity index 95% rename from src/lib/agent/__tests__/run-agent-standalone.test.ts rename to src/agent/__tests__/run-agent-standalone.test.ts index b8994dc93..9e436dbe4 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -17,14 +17,14 @@ import { type PendingQuestion, } from '@lib/wizard-session'; import { AGENT_ERROR_CODE, ErrorCodes } from '@shared/errors'; -import { AgentErrorType } from '@lib/agent/signals'; -import type { AgentFailure } from '@lib/agent/runner/shared/types'; -import type { AgentProgress } from '@lib/agent/progress'; +import { AgentErrorType } from '@agent/signals'; +import type { AgentFailure } from '@agent/runner/shared/types'; +import type { AgentProgress } from '@agent/progress'; import type { AgentHarness, BackendRunInputs, TaskRunInputs, -} from '@lib/agent/runner/harness/types'; +} from '@agent/runner/harness/types'; vi.mock('@ui', () => ({ getUI: () => { @@ -36,8 +36,8 @@ vi.mock('@ui', () => ({ })); vi.mock('@utils/debug'); vi.mock('@utils/terminal-bell'); -vi.mock('@lib/yara-hooks', async (original) => ({ - ...(await original()), +vi.mock('@agent/yara-hooks', async (original) => ({ + ...(await original()), flushScanReport: vi.fn(), })); vi.mock('@utils/analytics', () => ({ @@ -51,8 +51,8 @@ vi.mock('@utils/analytics', () => ({ shutdown: vi.fn().mockResolvedValue(undefined), }, })); -vi.mock('@lib/gateway-session', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent/gateway-session', async (importOriginal) => ({ + ...(await importOriginal()), gatewayAuth: vi.fn().mockResolvedValue({ gatewayUrl: 'https://gateway.test', token: 'phe_run', @@ -73,7 +73,7 @@ const harnessState = vi.hoisted(() => ({ seedFailure: undefined as AgentFailure | undefined, askQuestions: undefined as PendingQuestion['questions'] | undefined, })); -vi.mock('@lib/agent/runner/switchboard/harness', () => { +vi.mock('@agent/runner/switchboard/harness', () => { const askIfRequested = async (inputs: BackendRunInputs | TaskRunInputs) => { if (!harnessState.askQuestions || !inputs.askBridge) return; const { answers } = await inputs.askBridge.request({ @@ -149,10 +149,8 @@ vi.mock('@lib/agent/runner/switchboard/harness', () => { }; }); -vi.mock('@lib/agent/agent-prompt-loader', async (original) => { - const actual = await original< - typeof import('@lib/agent/agent-prompt-loader') - >(); +vi.mock('@agent/agent-prompt-loader', async (original) => { + const actual = await original(); return { ...actual, loadAgentRegistry: vi.fn(() => @@ -176,16 +174,16 @@ vi.mock('@lib/agent/agent-prompt-loader', async (original) => { ), }; }); -vi.mock('@lib/wizard-tools', async (original) => ({ - ...(await original()), +vi.mock('@agent/tools', async (original) => ({ + ...(await original()), fetchSkillMenu: vi.fn().mockResolvedValue({ categories: {} }), })); -import { runAgent, RunOutcome } from '@lib/agent/runner'; -import type { RunConfig, RunInput } from '@lib/agent/runner'; +import { runAgent, RunOutcome } from '@agent/runner'; +import type { RunConfig, RunInput } from '@agent/runner'; import { analytics } from '@utils/analytics'; import { initLogFile } from '@utils/debug'; -import { flushScanReport } from '@lib/yara-hooks'; +import { flushScanReport } from '@agent/yara-hooks'; import { QUEUE_DIR_NAME } from '../runner/sequence/orchestrator/queue'; let tmp: string; diff --git a/src/lib/agent/__tests__/run-tags.test.ts b/src/agent/__tests__/run-tags.test.ts similarity index 97% rename from src/lib/agent/__tests__/run-tags.test.ts rename to src/agent/__tests__/run-tags.test.ts index 8d88772ee..8211b96e8 100644 --- a/src/lib/agent/__tests__/run-tags.test.ts +++ b/src/agent/__tests__/run-tags.test.ts @@ -1,4 +1,4 @@ -import { buildRunTags } from '@lib/agent/agent-interface'; +import { buildRunTags } from '@agent/agent-interface'; import { CallType } from '@shared/constants'; describe('buildRunTags', () => { diff --git a/src/lib/agent/__tests__/settings-conflicts.test.ts b/src/agent/__tests__/settings-conflicts.test.ts similarity index 99% rename from src/lib/agent/__tests__/settings-conflicts.test.ts rename to src/agent/__tests__/settings-conflicts.test.ts index ba6f67a6a..e5fb24d64 100644 --- a/src/lib/agent/__tests__/settings-conflicts.test.ts +++ b/src/agent/__tests__/settings-conflicts.test.ts @@ -12,7 +12,7 @@ import { classifySettingsConflicts, } from '@shared/claude-settings'; import type { SettingsConflict } from '@shared/claude-settings'; -import { buildAuthErrorContext } from '@lib/agent/agent-interface'; +import { buildAuthErrorContext } from '@agent/agent-interface'; const OVERRIDE = JSON.stringify({ apiKeyHelper: 'echo sk-x' }); const ENV_OVERRIDE = JSON.stringify({ diff --git a/src/lib/agent/__tests__/stored-login.test.ts b/src/agent/__tests__/stored-login.test.ts similarity index 100% rename from src/lib/agent/__tests__/stored-login.test.ts rename to src/agent/__tests__/stored-login.test.ts diff --git a/src/lib/agent/__tests__/triage-provider.test.ts b/src/agent/__tests__/triage-provider.test.ts similarity index 97% rename from src/lib/agent/__tests__/triage-provider.test.ts rename to src/agent/__tests__/triage-provider.test.ts index f533c146e..4af277940 100644 --- a/src/lib/agent/__tests__/triage-provider.test.ts +++ b/src/agent/__tests__/triage-provider.test.ts @@ -1,12 +1,12 @@ import { completeSimple } from '@earendil-works/pi-ai'; -import { createTriageLLMProvider } from '@lib/agent/triage-provider'; +import { createTriageLLMProvider } from '@agent/triage-provider'; import { CallType, GPT5_6_LUNA_MODEL, HAIKU_TRIAGE_MODEL, Harness, } from '@shared/constants'; -import { triageModelFor } from '@lib/agent/runner/switchboard/models'; +import { triageModelFor } from '@agent/runner/switchboard/models'; vi.mock('@earendil-works/pi-ai', () => ({ completeSimple: vi.fn() })); const complete = vi.mocked(completeSimple); diff --git a/src/lib/agent/__tests__/tutorial-run-tags.test.ts b/src/agent/__tests__/tutorial-run-tags.test.ts similarity index 94% rename from src/lib/agent/__tests__/tutorial-run-tags.test.ts rename to src/agent/__tests__/tutorial-run-tags.test.ts index e0d2c2b3f..af00b50ef 100644 --- a/src/lib/agent/__tests__/tutorial-run-tags.test.ts +++ b/src/agent/__tests__/tutorial-run-tags.test.ts @@ -7,8 +7,8 @@ * tests pin the tags so that can't silently return. */ -import { buildTutorialRunTags } from '@lib/agent/mcp-prompt-streaming'; -import { buildAgentEnv } from '@lib/agent/agent-interface'; +import { buildTutorialRunTags } from '@agent/mcp-prompt-streaming'; +import { buildAgentEnv } from '@agent/agent-interface'; import { analytics } from '@utils/analytics'; describe('buildTutorialRunTags', () => { diff --git a/src/lib/agent/__tests__/variant-gating.test.ts b/src/agent/__tests__/variant-gating.test.ts similarity index 89% rename from src/lib/agent/__tests__/variant-gating.test.ts rename to src/agent/__tests__/variant-gating.test.ts index 1734e1446..7efd2ca49 100644 --- a/src/lib/agent/__tests__/variant-gating.test.ts +++ b/src/agent/__tests__/variant-gating.test.ts @@ -1,4 +1,4 @@ -import { isOrchestratorEnabled } from '@lib/agent/runner/switchboard'; +import { isOrchestratorEnabled } from '@agent/runner/switchboard'; describe('isOrchestratorEnabled', () => { it('is true only when the wizard-orchestrator flag is true', () => { diff --git a/src/lib/__tests__/wizard-ask-bridge.test.ts b/src/agent/__tests__/wizard-ask-bridge.test.ts similarity index 99% rename from src/lib/__tests__/wizard-ask-bridge.test.ts rename to src/agent/__tests__/wizard-ask-bridge.test.ts index 4cb0383a8..0f348ab17 100644 --- a/src/lib/__tests__/wizard-ask-bridge.test.ts +++ b/src/agent/__tests__/wizard-ask-bridge.test.ts @@ -2,7 +2,7 @@ import { CANCELLED_SENTINEL, createWizardAskBridge, isFullyCancelled, -} from '@lib/wizard-ask-bridge'; +} from '@agent/wizard-ask-bridge'; import { analytics } from '@utils/analytics'; import type { AskAnswers, PendingQuestion } from '@lib/wizard-session'; diff --git a/src/lib/__tests__/wizard-can-use-tool.test.ts b/src/agent/__tests__/wizard-can-use-tool.test.ts similarity index 99% rename from src/lib/__tests__/wizard-can-use-tool.test.ts rename to src/agent/__tests__/wizard-can-use-tool.test.ts index 90960aeeb..3d42f954f 100644 --- a/src/lib/__tests__/wizard-can-use-tool.test.ts +++ b/src/agent/__tests__/wizard-can-use-tool.test.ts @@ -1,4 +1,4 @@ -import { wizardCanUseTool } from '@lib/agent/agent-interface'; +import { wizardCanUseTool } from '@agent/agent-interface'; vi.mock('@utils/analytics', () => ({ analytics: { diff --git a/src/lib/__tests__/wizard-tools.test.ts b/src/agent/__tests__/wizard-tools.test.ts similarity index 99% rename from src/lib/__tests__/wizard-tools.test.ts rename to src/agent/__tests__/wizard-tools.test.ts index 5ddeff913..40bbd129e 100644 --- a/src/lib/__tests__/wizard-tools.test.ts +++ b/src/agent/__tests__/wizard-tools.test.ts @@ -28,7 +28,7 @@ import { resolveAskQuestionKinds, resolveEnvPath, templateEnvWriteRefusal, -} from '@lib/wizard-tools'; +} from '@agent/tools'; import type { AuditCheck } from '@lib/programs/audit/types'; function makeTmpDir(): string { diff --git a/src/lib/__tests__/yara-flush-report.test.ts b/src/agent/__tests__/yara-flush-report.test.ts similarity index 98% rename from src/lib/__tests__/yara-flush-report.test.ts rename to src/agent/__tests__/yara-flush-report.test.ts index 06e46e5c3..4c4a97b66 100644 --- a/src/lib/__tests__/yara-flush-report.test.ts +++ b/src/agent/__tests__/yara-flush-report.test.ts @@ -25,7 +25,7 @@ import { flushScanReport, recordExternalScan, resetScanReport, -} from '@lib/yara-hooks'; +} from '@agent/yara-hooks'; describe('flushScanReport', () => { beforeEach(() => { diff --git a/src/lib/__tests__/yara-hooks.test.ts b/src/agent/__tests__/yara-hooks.test.ts similarity index 99% rename from src/lib/__tests__/yara-hooks.test.ts rename to src/agent/__tests__/yara-hooks.test.ts index 8f3e8a338..b4cd76683 100644 --- a/src/lib/__tests__/yara-hooks.test.ts +++ b/src/agent/__tests__/yara-hooks.test.ts @@ -8,7 +8,7 @@ import { captureScanReport, recordExternalScan, resetScanReport, -} from '@lib/yara-hooks'; +} from '@agent/yara-hooks'; import { scan, triageMatches } from '@posthog/warlock'; import fs from 'fs'; import fg from 'fast-glob'; diff --git a/src/lib/__tests__/yara-policy.test.ts b/src/agent/__tests__/yara-policy.test.ts similarity index 100% rename from src/lib/__tests__/yara-policy.test.ts rename to src/agent/__tests__/yara-policy.test.ts diff --git a/src/lib/agent/agent-env-isolation.ts b/src/agent/agent-env-isolation.ts similarity index 100% rename from src/lib/agent/agent-env-isolation.ts rename to src/agent/agent-env-isolation.ts diff --git a/src/lib/agent/agent-interface.ts b/src/agent/agent-interface.ts similarity index 99% rename from src/lib/agent/agent-interface.ts rename to src/agent/agent-interface.ts index 3f5f41240..c418e5c19 100644 --- a/src/lib/agent/agent-interface.ts +++ b/src/agent/agent-interface.ts @@ -16,7 +16,7 @@ import type { WizardRunOptions } from '@utils/types'; import { analytics } from '@utils/analytics'; import { isTemplateEnvFileName } from '@utils/env-scan'; import { runtimeEnv } from '@env'; -import type { AioCapture } from '@lib/agent/aio-capture'; +import type { AioCapture } from '@agent/aio-capture'; import { Harness, CallType, @@ -36,14 +36,14 @@ import { gatewayAuth, isPastRefresh, type GatewayAuth, -} from '@lib/gateway-session'; +} from '@agent/gateway-session'; import { evaluateBashCommand } from './bash-fence'; -import { createWizardToolsServer, WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { createWizardToolsServer, WIZARD_TOOL_NAMES } from '@agent/tools'; import { createPreToolUseYaraHooks, createPostToolUseYaraHooks, prewarmYaraScanner, -} from '@lib/yara-hooks'; +} from '@agent/yara-hooks'; import { createTriageLLMProvider } from './triage-provider'; import type { LLMProvider } from '@posthog/warlock'; import { assembleCommandments } from './runner/switchboard/commandments'; @@ -218,7 +218,7 @@ export type AgentConfig = { */ modelOverride?: string; /** Bridge that drives the `wizard_ask` overlay. Omit in non-interactive hosts. */ - askBridge?: import('@lib/wizard-ask-bridge').WizardAskBridge; + askBridge?: import('@agent/wizard-ask-bridge').WizardAskBridge; /** Per-run cap on `wizard_ask` invocations. Defaults to 10. */ askMaxQuestions?: number; /** Extra tools added on top of BASE_ALLOWED_TOOLS for this run. */ @@ -235,7 +235,7 @@ export type AgentConfig = { * flag routes the run here; threaded into wizard-tools so the orchestrator * tools register. */ - orchestrator?: import('@lib/agent/runner/sequence/orchestrator/queue-tools').OrchestratorToolsContext; + orchestrator?: import('@agent/runner/sequence/orchestrator/queue-tools').OrchestratorToolsContext; /** * Optional AIO capture — mirrors each assistant SDK message into the * authenticated project as `$ai_generation`. No-op instance when diff --git a/src/lib/agent/agent-prompt-loader.ts b/src/agent/agent-prompt-loader.ts similarity index 99% rename from src/lib/agent/agent-prompt-loader.ts rename to src/agent/agent-prompt-loader.ts index e4372658f..bb4d591a4 100644 --- a/src/lib/agent/agent-prompt-loader.ts +++ b/src/agent/agent-prompt-loader.ts @@ -28,7 +28,7 @@ import { import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; import { fetchWithRetry } from '@shared/fetch-retry'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools/tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools/tools'; /** * The basics the client injects around every agent-prompt body. The `/agents/` diff --git a/src/lib/agent/agent-prompt.ts b/src/agent/agent-prompt.ts similarity index 100% rename from src/lib/agent/agent-prompt.ts rename to src/agent/agent-prompt.ts diff --git a/src/lib/agent/agent-runner.ts b/src/agent/agent-runner.ts similarity index 100% rename from src/lib/agent/agent-runner.ts rename to src/agent/agent-runner.ts diff --git a/src/lib/agent/aio-capture.ts b/src/agent/aio-capture.ts similarity index 100% rename from src/lib/agent/aio-capture.ts rename to src/agent/aio-capture.ts diff --git a/src/lib/agent/bash-fence.ts b/src/agent/bash-fence.ts similarity index 99% rename from src/lib/agent/bash-fence.ts rename to src/agent/bash-fence.ts index 1264b11af..caf31e77a 100644 --- a/src/lib/agent/bash-fence.ts +++ b/src/agent/bash-fence.ts @@ -13,7 +13,7 @@ * (`npx ` downloads and runs it), and shell injection. Matching is * token-exact per manager — keyword prefixes admitted `npm publish` via `pub`. */ -import { LINTING_TOOLS } from '@lib/safe-tools'; +import { LINTING_TOOLS } from '@agent/safe-tools'; export type BashFenceDecision = | { allowed: true } diff --git a/src/lib/agent/commandments.ts b/src/agent/commandments.ts similarity index 100% rename from src/lib/agent/commandments.ts rename to src/agent/commandments.ts diff --git a/src/lib/gateway-session.ts b/src/agent/gateway-session.ts similarity index 100% rename from src/lib/gateway-session.ts rename to src/agent/gateway-session.ts diff --git a/src/lib/agent/mcp-prompt-streaming.ts b/src/agent/mcp-prompt-streaming.ts similarity index 98% rename from src/lib/agent/mcp-prompt-streaming.ts rename to src/agent/mcp-prompt-streaming.ts index bf4b2414f..6bafc82fb 100644 --- a/src/lib/agent/mcp-prompt-streaming.ts +++ b/src/agent/mcp-prompt-streaming.ts @@ -15,10 +15,10 @@ import type { Credentials } from '@shared/api'; import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@shared/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 { createIsolatedAgentConfigDir } from '@lib/agent/stored-login'; +import { gatewayAuth } from '@agent/gateway-session'; +import { buildAgentEnv, buildRunTags } from '@agent/agent-interface'; +import { sanitizeAgentSubprocessEnv } from '@agent/agent-env-isolation'; +import { createIsolatedAgentConfigDir } from '@agent/stored-login'; import { analytics } from '@utils/analytics'; /** diff --git a/src/lib/middleware/__tests__/benchmark-emit.test.ts b/src/agent/middleware/__tests__/benchmark-emit.test.ts similarity index 98% rename from src/lib/middleware/__tests__/benchmark-emit.test.ts rename to src/agent/middleware/__tests__/benchmark-emit.test.ts index 2b1979e68..16bb16871 100644 --- a/src/lib/middleware/__tests__/benchmark-emit.test.ts +++ b/src/agent/middleware/__tests__/benchmark-emit.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { AgentProgress } from '@lib/agent/progress'; +import type { AgentProgress } from '@agent/progress'; // The benchmark pipeline runs inside the agent, which has no UI. Reaching one // is the defect this file guards against. diff --git a/src/lib/middleware/benchmark.ts b/src/agent/middleware/benchmark.ts similarity index 95% rename from src/lib/middleware/benchmark.ts rename to src/agent/middleware/benchmark.ts index 08c044253..bcbfad950 100644 --- a/src/lib/middleware/benchmark.ts +++ b/src/agent/middleware/benchmark.ts @@ -7,7 +7,7 @@ * pipeline.finalize(resultMessage, durationMs); */ -import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; +import type { ProgressEmitter, SpinnerHandle } from '@agent/progress'; import { logToFile, getLogFilePath, configureLogFile } from '@utils/debug'; import { MiddlewarePipeline } from './pipeline'; import { PhaseDetector } from './phase-detector'; @@ -15,7 +15,7 @@ import { loadBenchmarkConfig } from './config'; import { createPluginsFromConfig } from './benchmarks'; import type { BenchmarkConfig } from './config'; import type { WizardRunOptions } from '@utils/types'; -import { AgentSignals } from '@lib/agent/agent-interface'; +import { AgentSignals } from '@agent/agent-interface'; // ── Types ────────────────────────────────────────────────────────────── diff --git a/src/lib/middleware/benchmarks/cache-tracker.ts b/src/agent/middleware/benchmarks/cache-tracker.ts similarity index 99% rename from src/lib/middleware/benchmarks/cache-tracker.ts rename to src/agent/middleware/benchmarks/cache-tracker.ts index 047efc3a8..046a19ea3 100644 --- a/src/lib/middleware/benchmarks/cache-tracker.ts +++ b/src/agent/middleware/benchmarks/cache-tracker.ts @@ -8,7 +8,7 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import type { TurnData } from './turn-counter'; /** Matches SDK usage.cache_creation (ephemeral 5m vs 1h for pricing). */ diff --git a/src/lib/middleware/benchmarks/compaction-tracker.ts b/src/agent/middleware/benchmarks/compaction-tracker.ts similarity index 96% rename from src/lib/middleware/benchmarks/compaction-tracker.ts rename to src/agent/middleware/benchmarks/compaction-tracker.ts index bb16fb6e5..9f2dcc4dc 100644 --- a/src/lib/middleware/benchmarks/compaction-tracker.ts +++ b/src/agent/middleware/benchmarks/compaction-tracker.ts @@ -9,9 +9,9 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import { logToFile } from '@utils/debug'; -import { AgentSignals } from '@lib/agent/agent-interface'; +import { AgentSignals } from '@agent/agent-interface'; export interface CompactionData { phaseCompactions: number; diff --git a/src/lib/middleware/benchmarks/context-size-tracker.ts b/src/agent/middleware/benchmarks/context-size-tracker.ts similarity index 98% rename from src/lib/middleware/benchmarks/context-size-tracker.ts rename to src/agent/middleware/benchmarks/context-size-tracker.ts index f51f0f441..a43e4fe98 100644 --- a/src/lib/middleware/benchmarks/context-size-tracker.ts +++ b/src/agent/middleware/benchmarks/context-size-tracker.ts @@ -10,7 +10,7 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import type { TokenData } from './token-tracker'; export interface ContextSizeData { diff --git a/src/lib/middleware/benchmarks/cost-tracker.ts b/src/agent/middleware/benchmarks/cost-tracker.ts similarity index 99% rename from src/lib/middleware/benchmarks/cost-tracker.ts rename to src/agent/middleware/benchmarks/cost-tracker.ts index ce89091a9..9da769180 100644 --- a/src/lib/middleware/benchmarks/cost-tracker.ts +++ b/src/agent/middleware/benchmarks/cost-tracker.ts @@ -2,7 +2,7 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import { computeTokenCostUsd } from '@shared/token-pricing'; import type { TokenData } from './token-tracker'; import type { CacheData } from './cache-tracker'; diff --git a/src/lib/middleware/benchmarks/duration-tracker.ts b/src/agent/middleware/benchmarks/duration-tracker.ts similarity index 97% rename from src/lib/middleware/benchmarks/duration-tracker.ts rename to src/agent/middleware/benchmarks/duration-tracker.ts index 84427273c..abb553038 100644 --- a/src/lib/middleware/benchmarks/duration-tracker.ts +++ b/src/agent/middleware/benchmarks/duration-tracker.ts @@ -6,7 +6,7 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; export interface DurationData { phaseSnapshots: Array<{ diff --git a/src/lib/middleware/benchmarks/index.ts b/src/agent/middleware/benchmarks/index.ts similarity index 95% rename from src/lib/middleware/benchmarks/index.ts rename to src/agent/middleware/benchmarks/index.ts index 33491a8db..35d672237 100644 --- a/src/lib/middleware/benchmarks/index.ts +++ b/src/agent/middleware/benchmarks/index.ts @@ -8,8 +8,8 @@ import type { Middleware, MiddlewareFactoryOptions, -} from '@lib/middleware/types'; -import type { BenchmarkConfig } from '@lib/middleware/config'; +} from '@agent/middleware/types'; +import type { BenchmarkConfig } from '@agent/middleware/config'; import { TurnCounterPlugin } from './turn-counter'; import { TokenTrackerPlugin } from './token-tracker'; import { CacheTrackerPlugin } from './cache-tracker'; diff --git a/src/lib/middleware/benchmarks/json-writer.ts b/src/agent/middleware/benchmarks/json-writer.ts similarity index 96% rename from src/lib/middleware/benchmarks/json-writer.ts rename to src/agent/middleware/benchmarks/json-writer.ts index a68cad9c6..5c9ab8ad1 100644 --- a/src/lib/middleware/benchmarks/json-writer.ts +++ b/src/agent/middleware/benchmarks/json-writer.ts @@ -6,14 +6,14 @@ */ import fs from 'fs'; -import type { ProgressEmitter } from '@lib/agent/progress'; +import type { ProgressEmitter } from '@agent/progress'; import { logToFile } from '@utils/debug'; -import { AgentSignals } from '@lib/agent/agent-interface'; +import { AgentSignals } from '@agent/agent-interface'; import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import type { TokenData } from './token-tracker'; import type { CacheData } from './cache-tracker'; import type { TurnData } from './turn-counter'; @@ -21,7 +21,7 @@ import type { CostData } from './cost-tracker'; import type { DurationData } from './duration-tracker'; import type { CompactionData } from './compaction-tracker'; import type { ContextSizeData } from './context-size-tracker'; -import type { BenchmarkData, StepUsage } from '@lib/middleware/benchmark'; +import type { BenchmarkData, StepUsage } from '@agent/middleware/benchmark'; /** * Sum token usage across all models from the SDK's modelUsage field. diff --git a/src/lib/middleware/benchmarks/summary.ts b/src/agent/middleware/benchmarks/summary.ts similarity index 96% rename from src/lib/middleware/benchmarks/summary.ts rename to src/agent/middleware/benchmarks/summary.ts index be1369561..0c12a0024 100644 --- a/src/lib/middleware/benchmarks/summary.ts +++ b/src/agent/middleware/benchmarks/summary.ts @@ -1,10 +1,10 @@ -import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; -import { AgentSignals } from '@lib/agent/agent-interface'; +import type { ProgressEmitter, SpinnerHandle } from '@agent/progress'; +import { AgentSignals } from '@agent/agent-interface'; import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import type { TokenData } from './token-tracker'; import type { TurnData } from './turn-counter'; import type { CostData } from './cost-tracker'; diff --git a/src/lib/middleware/benchmarks/token-tracker.ts b/src/agent/middleware/benchmarks/token-tracker.ts similarity index 98% rename from src/lib/middleware/benchmarks/token-tracker.ts rename to src/agent/middleware/benchmarks/token-tracker.ts index e709bc75b..6a49b1355 100644 --- a/src/lib/middleware/benchmarks/token-tracker.ts +++ b/src/agent/middleware/benchmarks/token-tracker.ts @@ -11,7 +11,7 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; import type { TurnData } from './turn-counter'; export interface TokenData { diff --git a/src/lib/middleware/benchmarks/turn-counter.ts b/src/agent/middleware/benchmarks/turn-counter.ts similarity index 98% rename from src/lib/middleware/benchmarks/turn-counter.ts rename to src/agent/middleware/benchmarks/turn-counter.ts index 0012f8595..293168c06 100644 --- a/src/lib/middleware/benchmarks/turn-counter.ts +++ b/src/agent/middleware/benchmarks/turn-counter.ts @@ -10,7 +10,7 @@ import type { Middleware, MiddlewareContext, MiddlewareStore, -} from '@lib/middleware/types'; +} from '@agent/middleware/types'; export interface TurnData { /** Whether the current message is a duplicate of the last processed turn */ diff --git a/src/lib/middleware/config.ts b/src/agent/middleware/config.ts similarity index 98% rename from src/lib/middleware/config.ts rename to src/agent/middleware/config.ts index 5d45af984..e5d3a912c 100644 --- a/src/lib/middleware/config.ts +++ b/src/agent/middleware/config.ts @@ -8,7 +8,7 @@ import fs from 'fs'; import path from 'path'; import { logToFile } from '@utils/debug'; -import { AgentSignals } from '@lib/agent/agent-interface'; +import { AgentSignals } from '@agent/agent-interface'; import { runtimeEnv } from '@env'; import { WIZARD_BENCHMARK_FILE, WIZARD_LOG_FILE } from '@utils/paths'; diff --git a/src/lib/middleware/index.ts b/src/agent/middleware/index.ts similarity index 100% rename from src/lib/middleware/index.ts rename to src/agent/middleware/index.ts diff --git a/src/lib/middleware/phase-detector.ts b/src/agent/middleware/phase-detector.ts similarity index 100% rename from src/lib/middleware/phase-detector.ts rename to src/agent/middleware/phase-detector.ts diff --git a/src/lib/middleware/pipeline.ts b/src/agent/middleware/pipeline.ts similarity index 100% rename from src/lib/middleware/pipeline.ts rename to src/agent/middleware/pipeline.ts diff --git a/src/lib/middleware/types.ts b/src/agent/middleware/types.ts similarity index 96% rename from src/lib/middleware/types.ts rename to src/agent/middleware/types.ts index 8ccd15ec9..a8fa2aa2e 100644 --- a/src/lib/middleware/types.ts +++ b/src/agent/middleware/types.ts @@ -5,7 +5,7 @@ * and can publish data to a shared store for downstream middleware to read. */ -import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; +import type { ProgressEmitter, SpinnerHandle } from '@agent/progress'; export type SDKMessage = any; diff --git a/src/lib/agent/output-signals.ts b/src/agent/output-signals.ts similarity index 100% rename from src/lib/agent/output-signals.ts rename to src/agent/output-signals.ts diff --git a/src/lib/agent/progress.ts b/src/agent/progress.ts similarity index 100% rename from src/lib/agent/progress.ts rename to src/agent/progress.ts diff --git a/src/lib/agent/runner/README.md b/src/agent/runner/README.md similarity index 100% rename from src/lib/agent/runner/README.md rename to src/agent/runner/README.md diff --git a/src/lib/agent/runner/__tests__/switchboard.test.ts b/src/agent/runner/__tests__/switchboard.test.ts similarity index 98% rename from src/lib/agent/runner/__tests__/switchboard.test.ts rename to src/agent/runner/__tests__/switchboard.test.ts index 3ae1c283a..6b4f0e8fd 100644 --- a/src/lib/agent/runner/__tests__/switchboard.test.ts +++ b/src/agent/runner/__tests__/switchboard.test.ts @@ -27,7 +27,7 @@ import { DEFAULT_BINDING, resolveBinding, type SwitchboardCtx, -} from '@lib/agent/runner/switchboard'; +} from '@agent/runner/switchboard'; import { modelCapabilities, MINT_ALLOWED_EFFORTS, @@ -35,8 +35,8 @@ import { requireKnownModel, TRIAGE_MODELS, VALID_MODELS, -} from '@lib/agent/runner/switchboard/models'; -import { runBindingCases } from '@lib/agent/runner/switchboard/flags/__tests__/binding-cases'; +} from '@agent/runner/switchboard/models'; +import { runBindingCases } from '@agent/runner/switchboard/flags/__tests__/binding-cases'; const PROGRAM_IDS = PROGRAM_REGISTRY.map((c) => c.id); const DEFAULT_RESOLVED = { diff --git a/src/lib/agent/runner/harness/agents-platform/README.md b/src/agent/runner/harness/agents-platform/README.md similarity index 100% rename from src/lib/agent/runner/harness/agents-platform/README.md rename to src/agent/runner/harness/agents-platform/README.md diff --git a/src/lib/agent/runner/harness/anthropic/README.md b/src/agent/runner/harness/anthropic/README.md similarity index 100% rename from src/lib/agent/runner/harness/anthropic/README.md rename to src/agent/runner/harness/anthropic/README.md diff --git a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts b/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts similarity index 95% rename from src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts rename to src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts index aa4ed9038..e4b32366e 100644 --- a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts +++ b/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts @@ -1,4 +1,4 @@ -import { initializeAgent, wizardCanUseTool } from '@lib/agent/agent-interface'; +import { initializeAgent, wizardCanUseTool } from '@agent/agent-interface'; import { createAskBridge } from '../../../shared/ask'; import { anthropicBackend } from '..'; import type { BackendRunInputs, TaskRunInputs } from '../../types'; @@ -8,9 +8,9 @@ import { HostResolution } from '@shared/host-resolution'; vi.mock('@utils/analytics'); vi.mock('@utils/debug'); -vi.mock('@lib/agent/aio-capture', () => ({ createAioCapture: vi.fn() })); -vi.mock('@lib/agent/agent-interface', async (original) => ({ - ...(await original()), +vi.mock('@agent/aio-capture', () => ({ createAioCapture: vi.fn() })); +vi.mock('@agent/agent-interface', async (original) => ({ + ...(await original()), initializeAgent: vi.fn().mockResolvedValue({}), runAgent: vi.fn().mockResolvedValue({}), })); diff --git a/src/lib/agent/runner/harness/anthropic/index.ts b/src/agent/runner/harness/anthropic/index.ts similarity index 95% rename from src/lib/agent/runner/harness/anthropic/index.ts rename to src/agent/runner/harness/anthropic/index.ts index 3460b3e27..74a051420 100644 --- a/src/lib/agent/runner/harness/anthropic/index.ts +++ b/src/agent/runner/harness/anthropic/index.ts @@ -4,12 +4,12 @@ import { Harness } from '@shared/constants'; import { initializeAgent, runAgent as executeAgent, -} from '@lib/agent/agent-interface'; -import { createAioCapture } from '@lib/agent/aio-capture'; +} from '@agent/agent-interface'; +import { createAioCapture } from '@agent/aio-capture'; import { getLogFilePath, logToFile } from '@utils/debug'; import { detectNodePackageManagers } from '@utils/package-manager'; -import { runOptions } from '@lib/agent/runner/shared/bootstrap'; -import { createEmitLog } from '@lib/agent/runner/shared/progress-collector'; +import { runOptions } from '@agent/runner/shared/bootstrap'; +import { createEmitLog } from '@agent/runner/shared/progress-collector'; import type { AgentResult, AgentHarness, diff --git a/src/lib/agent/runner/harness/pi/README.md b/src/agent/runner/harness/pi/README.md similarity index 100% rename from src/lib/agent/runner/harness/pi/README.md rename to src/agent/runner/harness/pi/README.md diff --git a/src/lib/agent/runner/harness/pi/__tests__/completion.test.ts b/src/agent/runner/harness/pi/__tests__/completion.test.ts similarity index 97% rename from src/lib/agent/runner/harness/pi/__tests__/completion.test.ts rename to src/agent/runner/harness/pi/__tests__/completion.test.ts index a378c2d6d..f451bcc41 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/completion.test.ts +++ b/src/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 '@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__/env-lockdown.test.ts b/src/agent/runner/harness/pi/__tests__/env-lockdown.test.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/__tests__/env-lockdown.test.ts rename to src/agent/runner/harness/pi/__tests__/env-lockdown.test.ts diff --git a/src/lib/agent/runner/harness/pi/__tests__/gateway.test.ts b/src/agent/runner/harness/pi/__tests__/gateway.test.ts similarity index 99% rename from src/lib/agent/runner/harness/pi/__tests__/gateway.test.ts rename to src/agent/runner/harness/pi/__tests__/gateway.test.ts index c68420193..cfedba108 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/gateway.test.ts +++ b/src/agent/runner/harness/pi/__tests__/gateway.test.ts @@ -5,7 +5,7 @@ import { withGatewayRemint, GATEWAY_PROVIDER, } from '../gateway'; -import type { GatewayAuth } from '@lib/gateway-session'; +import type { GatewayAuth } from '@agent/gateway-session'; describe('buildGatewayProvider effort', () => { const base = { diff --git a/src/lib/agent/runner/harness/pi/__tests__/security.test.ts b/src/agent/runner/harness/pi/__tests__/security.test.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/__tests__/security.test.ts rename to src/agent/runner/harness/pi/__tests__/security.test.ts diff --git a/src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts b/src/agent/runner/harness/pi/__tests__/status-line.test.ts similarity index 96% rename from src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts rename to src/agent/runner/harness/pi/__tests__/status-line.test.ts index b2920ab18..f7db0f8d2 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/status-line.test.ts +++ b/src/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 '@agent/signals'; import { lastStatusLine } from '..'; const S = AgentSignals.STATUS; // '[STATUS]' diff --git a/src/lib/agent/runner/harness/pi/__tests__/task-tools.test.ts b/src/agent/runner/harness/pi/__tests__/task-tools.test.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/__tests__/task-tools.test.ts rename to src/agent/runner/harness/pi/__tests__/task-tools.test.ts diff --git a/src/lib/agent/runner/harness/pi/__tests__/tools.test.ts b/src/agent/runner/harness/pi/__tests__/tools.test.ts similarity index 99% rename from src/lib/agent/runner/harness/pi/__tests__/tools.test.ts rename to src/agent/runner/harness/pi/__tests__/tools.test.ts index c3ef60dc1..f8f69d156 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/tools.test.ts +++ b/src/agent/runner/harness/pi/__tests__/tools.test.ts @@ -12,7 +12,7 @@ import { describe, it, expect, vi } from 'vitest'; import { CANCELLED_SENTINEL, type WizardAskBridge, -} from '@lib/wizard-ask-bridge'; +} from '@agent/wizard-ask-bridge'; import { createWizardPiTools } from '../tools'; import { evaluateToolCall } from '../security'; import { allowedPiCodingTools, allowedOrchestratorTools } from '../task'; @@ -24,7 +24,7 @@ import { WIZARD_ASK_SENSITIVE_DESCRIPTION, WIZARD_ASK_SUBJECT_DESCRIPTION, WIZARD_ASK_TOOL_DESCRIPTION, -} from '@lib/wizard-tools/tools'; +} from '@agent/tools/tools'; const SECRET = 'phx_live_zendesk_token_123'; diff --git a/src/lib/agent/runner/harness/pi/completion.ts b/src/agent/runner/harness/pi/completion.ts similarity index 95% rename from src/lib/agent/runner/harness/pi/completion.ts rename to src/agent/runner/harness/pi/completion.ts index 3d4c9d606..8219e5c46 100644 --- a/src/lib/agent/runner/harness/pi/completion.ts +++ b/src/agent/runner/harness/pi/completion.ts @@ -1,4 +1,4 @@ -import { AgentErrorType } from '@lib/agent/signals'; +import { AgentErrorType } from '@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/gateway.ts b/src/agent/runner/harness/pi/gateway.ts similarity index 99% rename from src/lib/agent/runner/harness/pi/gateway.ts rename to src/agent/runner/harness/pi/gateway.ts index c827bcfcb..9c6a6d6c7 100644 --- a/src/lib/agent/runner/harness/pi/gateway.ts +++ b/src/agent/runner/harness/pi/gateway.ts @@ -10,7 +10,7 @@ import { buildWizardPropertiesBlob, isPastRefresh, type GatewayAuth, -} from '@lib/gateway-session'; +} from '@agent/gateway-session'; import { modelCapabilities, type ThinkingLevel, diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/agent/runner/harness/pi/index.ts similarity index 97% rename from src/lib/agent/runner/harness/pi/index.ts rename to src/agent/runner/harness/pi/index.ts index 0b677347c..ec903a4e9 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/agent/runner/harness/pi/index.ts @@ -22,26 +22,26 @@ import { WIZARD_USER_AGENT, } from '@shared/constants'; import { analytics } from '@utils/analytics'; -import { AgentErrorType } from '@lib/agent/agent-interface'; -import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent/signals'; -import { AgentOutputSignals } from '@lib/agent/output-signals'; +import { AgentErrorType } from '@agent/agent-interface'; +import { AgentSignals, REMARK_INSTRUCTION } from '@agent/signals'; +import { AgentOutputSignals } from '@agent/output-signals'; import { assembleCommandments } from '../../switchboard/commandments'; -import { gatewayAuth, type GatewayAuth } from '@lib/gateway-session'; +import { gatewayAuth, type GatewayAuth } from '@agent/gateway-session'; import { buildGatewayProvider, GATEWAY_PROVIDER, withGatewayRemint, } from './gateway'; -import { createAioCapture } from '@lib/agent/aio-capture'; +import { createAioCapture } from '@agent/aio-capture'; import type { AgentResult, AgentHarness, BackendRunInputs, TaskRunInputs, } from '../types'; -import type { BootstrapResult } from '@lib/agent/runner/shared/types'; -import type { ProgressEmitter } from '@lib/agent/progress'; -import { createEmitLog } from '@lib/agent/runner/shared/progress-collector'; +import type { BootstrapResult } from '@agent/runner/shared/types'; +import type { ProgressEmitter } from '@agent/progress'; +import { createEmitLog } from '@agent/runner/shared/progress-collector'; import type { TaskStore } from './tasks'; import { completionFailure, runErrorType } from './completion'; @@ -327,7 +327,7 @@ export const piBackend: AgentHarness = { // Pay warlock's WASM-init + rule-compile cost now, off the tool-call // path, so the first scanned call doesn't eat cold-start latency. - const { prewarmYaraScanner } = await import('@lib/yara-hooks'); + const { prewarmYaraScanner } = await import('@agent/yara-hooks'); void prewarmYaraScanner(); // Wire the real PostHog MCP into pi (#10): load pi's MCP adapter and point diff --git a/src/lib/agent/runner/harness/pi/mcp.ts b/src/agent/runner/harness/pi/mcp.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/mcp.ts rename to src/agent/runner/harness/pi/mcp.ts diff --git a/src/lib/agent/runner/harness/pi/orchestrator-tools.ts b/src/agent/runner/harness/pi/orchestrator-tools.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/orchestrator-tools.ts rename to src/agent/runner/harness/pi/orchestrator-tools.ts diff --git a/src/lib/agent/runner/harness/pi/runtime-notes.ts b/src/agent/runner/harness/pi/runtime-notes.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/runtime-notes.ts rename to src/agent/runner/harness/pi/runtime-notes.ts diff --git a/src/lib/agent/runner/harness/pi/security.ts b/src/agent/runner/harness/pi/security.ts similarity index 99% rename from src/lib/agent/runner/harness/pi/security.ts rename to src/agent/runner/harness/pi/security.ts index 0ff5c811c..ffb399712 100644 --- a/src/lib/agent/runner/harness/pi/security.ts +++ b/src/agent/runner/harness/pi/security.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import path from 'path'; import type { LLMProvider, ScanMatch } from '@posthog/warlock'; -import { wizardCanUseTool } from '@lib/agent/agent-interface'; +import { wizardCanUseTool } from '@agent/agent-interface'; import { createRepeatBlockTracker, isWizardDocumentationPath, @@ -28,12 +28,12 @@ import { repeatBlockReason, scanAndTriage, type RepeatBlockTracker, -} from '@lib/yara-hooks'; +} from '@agent/yara-hooks'; import { publishBlockingMatch, scanVerdict, type ScanContext, -} from '@lib/yara-policy'; +} from '@agent/yara-policy'; import { logToFile } from '@utils/debug'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/agent/runner/harness/pi/subagent.ts b/src/agent/runner/harness/pi/subagent.ts similarity index 100% rename from src/lib/agent/runner/harness/pi/subagent.ts rename to src/agent/runner/harness/pi/subagent.ts diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/agent/runner/harness/pi/task.ts similarity index 97% rename from src/lib/agent/runner/harness/pi/task.ts rename to src/agent/runner/harness/pi/task.ts index 90f08530a..76db7a43a 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/agent/runner/harness/pi/task.ts @@ -28,14 +28,14 @@ import { allowsPostHogMcp, queueTools, renderToolInventory, -} from '@lib/agent/agent-prompt-loader'; -import { AgentErrorType } from '@lib/agent/agent-interface'; -import { REMARK_INSTRUCTION } from '@lib/agent/signals'; -import { AgentOutputSignals } from '@lib/agent/output-signals'; +} from '@agent/agent-prompt-loader'; +import { AgentErrorType } from '@agent/agent-interface'; +import { REMARK_INSTRUCTION } from '@agent/signals'; +import { AgentOutputSignals } from '@agent/output-signals'; import { TaskStatus } from '../../sequence/orchestrator/queue'; import type { OrchestratorToolsContext } from '../../sequence/orchestrator/queue-tools'; import type { AgentResult, TaskRunInputs } from '../types'; -import { gatewayAuth, type GatewayAuth } from '@lib/gateway-session'; +import { gatewayAuth, type GatewayAuth } from '@agent/gateway-session'; import { buildGatewayProvider, GATEWAY_PROVIDER, @@ -50,7 +50,7 @@ import { lastStatusLine, withMode, } from './index'; -import { createAioCapture } from '@lib/agent/aio-capture'; +import { createAioCapture } from '@agent/aio-capture'; /** wizard tool vocabulary → the pi tool definitions it unlocks. */ const CODING_TOOL_MAP: Record = { @@ -282,7 +282,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { triageProvider: boot.triageProvider, getWizardAskPending: () => askState.pending, }); - const { prewarmYaraScanner } = await import('@lib/yara-hooks'); + const { prewarmYaraScanner } = await import('@agent/yara-hooks'); void prewarmYaraScanner(); // PostHog MCP, for the tasks whose prompt requests it. Tasks that never diff --git a/src/lib/agent/runner/harness/pi/tasks.ts b/src/agent/runner/harness/pi/tasks.ts similarity index 98% rename from src/lib/agent/runner/harness/pi/tasks.ts rename to src/agent/runner/harness/pi/tasks.ts index 0ee0f4462..61025abb6 100644 --- a/src/lib/agent/runner/harness/pi/tasks.ts +++ b/src/agent/runner/harness/pi/tasks.ts @@ -9,7 +9,7 @@ import { Type } from 'typebox'; import { defineTool } from '@earendil-works/pi-coding-agent'; import type { ToolDefinition } from '@earendil-works/pi-coding-agent'; -import type { TaskSnapshot } from '@lib/agent/progress'; +import type { TaskSnapshot } from '@agent/progress'; export type TaskStatus = 'pending' | 'in_progress' | 'completed'; export interface TaskEntry { diff --git a/src/lib/agent/runner/harness/pi/tools.ts b/src/agent/runner/harness/pi/tools.ts similarity index 99% rename from src/lib/agent/runner/harness/pi/tools.ts rename to src/agent/runner/harness/pi/tools.ts index 7b1b66f44..06c752160 100644 --- a/src/lib/agent/runner/harness/pi/tools.ts +++ b/src/agent/runner/harness/pi/tools.ts @@ -52,16 +52,19 @@ import { WIZARD_ASK_SENSITIVE_DESCRIPTION, WIZARD_ASK_SUBJECT_DESCRIPTION, WIZARD_ASK_TOOL_DESCRIPTION, -} from '@lib/wizard-tools/tools'; +} from '@agent/tools/tools'; import type { LLMProvider } from '@posthog/warlock'; -import type { ProgressEmitter } from '@lib/agent/progress'; -import { isFullyCancelled, type WizardAskBridge } from '@lib/wizard-ask-bridge'; +import type { ProgressEmitter } from '@agent/progress'; +import { + isFullyCancelled, + type WizardAskBridge, +} from '@agent/wizard-ask-bridge'; import { PUBLISH_HANDOFF_CONTENT_DESCRIPTION, PUBLISH_HANDOFF_DESCRIPTION, PUBLISH_HANDOFF_TOOL_NAME, publishHandoff, -} from '@lib/wizard-tools/handoff'; +} from '@agent/tools/handoff'; import { createSecretVault } from '@shared/secret-vault'; import { AUDIT_CHECKS_FILE, diff --git a/src/lib/agent/runner/harness/types.ts b/src/agent/runner/harness/types.ts similarity index 93% rename from src/lib/agent/runner/harness/types.ts rename to src/agent/runner/harness/types.ts index b4a2d64eb..80d4786c8 100644 --- a/src/lib/agent/runner/harness/types.ts +++ b/src/agent/runner/harness/types.ts @@ -21,20 +21,20 @@ import type { AdditionalFeature } from '@shared/constants'; import type { Harness } from '@shared/constants'; -import type { WizardAskBridge } from '@lib/wizard-ask-bridge'; -import type { AgentErrorType } from '@lib/agent/agent-interface'; -import type { ProgressEmitter, SpinnerHandle } from '@lib/agent/progress'; -import type { OrchestratorToolsContext } from '@lib/agent/runner/sequence/orchestrator/queue-tools'; +import type { WizardAskBridge } from '@agent/wizard-ask-bridge'; +import type { AgentErrorType } from '@agent/agent-interface'; +import type { ProgressEmitter, SpinnerHandle } from '@agent/progress'; +import type { OrchestratorToolsContext } from '@agent/runner/sequence/orchestrator/queue-tools'; import type { EffortLevel, ThinkingLevel, -} from '@lib/agent/runner/switchboard/models'; +} from '@agent/runner/switchboard/models'; import type { AgentFailure, BootstrapResult, RunConfig, RunInput, -} from '@lib/agent/runner/shared/types'; +} from '@agent/runner/shared/types'; /** The benchmark/telemetry hook threaded through a run, if enabled. */ export interface RunMiddleware { diff --git a/src/lib/agent/runner/index.ts b/src/agent/runner/index.ts similarity index 98% rename from src/lib/agent/runner/index.ts rename to src/agent/runner/index.ts index 67e06c50f..9f583e16c 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -36,7 +36,7 @@ import type { import { prepareRun } from './shared/bootstrap'; import { createProgressCollector } from './shared/progress-collector'; import { getSequence } from './switchboard'; -import { flushScanReport } from '../../yara-hooks'; +import { flushScanReport } from '@agent/yara-hooks'; export type { AbortCase, @@ -59,7 +59,7 @@ export type { AgentInteraction, AgentProgress, ProgressEmitter, -} from '@lib/agent/progress'; +} from '@agent/progress'; export { shouldDisableAsk } from './shared/bootstrap'; export { resolveBinding } from './switchboard'; export type { ProgramBinding, SwitchboardCtx } from './switchboard'; diff --git a/src/lib/agent/runner/sequence/README.md b/src/agent/runner/sequence/README.md similarity index 100% rename from src/lib/agent/runner/sequence/README.md rename to src/agent/runner/sequence/README.md diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/agent/runner/sequence/linear.ts similarity index 97% rename from src/lib/agent/runner/sequence/linear.ts rename to src/agent/runner/sequence/linear.ts index afdf90084..e86ccbf4d 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/agent/runner/sequence/linear.ts @@ -10,14 +10,14 @@ * arguments, so the caller's exit sequence is unchanged. */ -import { OutroKind, type OutroData } from '@lib/agent/progress'; +import { OutroKind, type OutroData } from '@agent/progress'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; import { logToFile } from '@utils/debug'; -import { createBenchmarkPipeline } from '../../../middleware/benchmark'; +import { createBenchmarkPipeline } from '@agent/middleware/benchmark'; import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@shared/errors'; import { analytics } from '@utils/analytics'; -import { formatYaraAbortMessage } from '../../../yara-hooks'; -import { installSkillById } from '../../../wizard-tools'; +import { formatYaraAbortMessage } from '@agent/yara-hooks'; +import { installSkillById } from '@agent/tools'; import { assemblePrompt } from '../../agent-prompt'; import type { SequenceResult, SequenceContext } from '../shared/types'; import { failed, installFailure } from '../shared/errors'; diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts similarity index 93% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts index 49e60ce34..fa3446a38 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/completed-seeded-types.test.ts @@ -16,8 +16,8 @@ vi.mock('@utils/analytics', () => ({ import { QueueStore, SkipReason, -} from '@lib/agent/runner/sequence/orchestrator/queue'; -import { completedSeededTypes } from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +} from '@agent/runner/sequence/orchestrator/queue'; +import { completedSeededTypes } from '@agent/runner/sequence/orchestrator/orchestrator-runner'; describe('completedSeededTypes', () => { let dir: string; diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts similarity index 98% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts index c76f75e86..a5236f1f5 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/executor.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts @@ -6,12 +6,12 @@ import { TaskStatus, type QueuedTask, type TaskHandoff, -} from '@lib/agent/runner/sequence/orchestrator/queue'; +} from '@agent/runner/sequence/orchestrator/queue'; import { drainQueue, RunTaskFatal, type RunTask, -} from '@lib/agent/runner/sequence/orchestrator/executor'; +} from '@agent/runner/sequence/orchestrator/executor'; vi.mock('@utils/analytics', () => ({ analytics: { captureException: vi.fn(), wizardCapture: vi.fn() }, diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/handoff-schema-parity.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/handoff-schema-parity.test.ts similarity index 100% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/handoff-schema-parity.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/handoff-schema-parity.test.ts diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts similarity index 90% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts index 85e130bce..0a3724258 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/optional-tasks.test.ts @@ -7,8 +7,8 @@ vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn(), captureException: vi.fn() }, })); -import { QueueStore } from '@lib/agent/runner/sequence/orchestrator/queue'; -import { drainVerdict } from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +import { QueueStore } from '@agent/runner/sequence/orchestrator/queue'; +import { drainVerdict } from '@agent/runner/sequence/orchestrator/orchestrator-runner'; describe('drainVerdict', () => { let dir: string; diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts similarity index 97% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts index 3ee223d9e..4f2d78d30 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/queue-tools.test.ts @@ -7,7 +7,7 @@ import { NotNeededReason, QueueStore, SkipReason, -} from '@lib/agent/runner/sequence/orchestrator/queue'; +} from '@agent/runner/sequence/orchestrator/queue'; vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn() }, @@ -22,12 +22,9 @@ import { ENQUEUE_MODEL_DESCRIPTION, NOT_NEEDED_REASON_ASK, type OrchestratorToolsContext, -} from '@lib/agent/runner/sequence/orchestrator/queue-tools'; -import { PI_COMPLETE_PARAM_KEYS } from '@lib/agent/runner/harness/pi/orchestrator-tools'; -import { - isValidModel, - VALID_MODELS, -} from '@lib/agent/runner/switchboard/models'; +} from '@agent/runner/sequence/orchestrator/queue-tools'; +import { PI_COMPLETE_PARAM_KEYS } from '@agent/runner/harness/pi/orchestrator-tools'; +import { isValidModel, VALID_MODELS } from '@agent/runner/switchboard/models'; function tmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'queue-tools-test-')); diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/queue.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/queue.test.ts similarity index 99% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/queue.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/queue.test.ts index cb306ef5b..6d7fab6c5 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/queue.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/queue.test.ts @@ -11,7 +11,7 @@ import { type QueuedTask, type TaskHandoff, type TransitionEvent, -} from '@lib/agent/runner/sequence/orchestrator/queue'; +} from '@agent/runner/sequence/orchestrator/queue'; vi.mock('@utils/analytics', () => ({ analytics: { captureException: vi.fn(), wizardCapture: vi.fn() }, diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts similarity index 97% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts index da82af353..d86f9257c 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/run-metrics.test.ts @@ -1,4 +1,4 @@ -import { RunMetrics } from '@lib/agent/runner/sequence/orchestrator/run-metrics'; +import { RunMetrics } from '@agent/runner/sequence/orchestrator/run-metrics'; describe('RunMetrics', () => { it('reports time to first start and first completion from run start', () => { diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts similarity index 96% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts index 9ee3fca2a..611543afc 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/seeded-decline-skip.test.ts @@ -22,14 +22,14 @@ vi.mock('@utils/analytics', () => ({ import { skipDeclinedSeededTasks, type SeededConsent, -} from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +} from '@agent/runner/sequence/orchestrator/orchestrator-runner'; import { QueueStore, SkipReason, TaskStatus, type TransitionEvent, type QueuedTask, -} from '@lib/agent/runner/sequence/orchestrator/queue'; +} from '@agent/runner/sequence/orchestrator/queue'; const KEPT: SeededConsent = { keep: true, timedOut: false, errored: false }; const DECLINED: SeededConsent = { diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts similarity index 99% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts index 2a3069188..c12548838 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/seeded-deps.test.ts @@ -17,11 +17,11 @@ vi.mock('@utils/analytics', () => ({ import { QueueStore, type QueuedTask, -} from '@lib/agent/runner/sequence/orchestrator/queue'; +} from '@agent/runner/sequence/orchestrator/queue'; import { deferSeededTasks, seededDependencies, -} from '@lib/agent/runner/sequence/orchestrator/seeded-deps'; +} from '@agent/runner/sequence/orchestrator/seeded-deps'; const SINKS = ['report']; diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts similarity index 98% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts index 9372bfc1d..4b7cb6b28 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/sink-closure.test.ts @@ -14,16 +14,16 @@ vi.mock('@utils/analytics', () => ({ import { QueueStore, SkipReason, -} from '@lib/agent/runner/sequence/orchestrator/queue'; -import { displayOrder } from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +} from '@agent/runner/sequence/orchestrator/queue'; +import { displayOrder } from '@agent/runner/sequence/orchestrator/orchestrator-runner'; import { applyEnqueue, checkEnqueueGuards, dependencyClosure, uncoveredBySink, type OrchestratorToolsContext, -} from '@lib/agent/runner/sequence/orchestrator/queue-tools'; -import { seededDependencies } from '@lib/agent/runner/sequence/orchestrator/seeded-deps'; +} from '@agent/runner/sequence/orchestrator/queue-tools'; +import { seededDependencies } from '@agent/runner/sequence/orchestrator/seeded-deps'; const VALID = ['warehouse', 'install', 'capture', 'report']; const SINKS = ['report']; diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts similarity index 98% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts index 6a04c2ab8..fc26b1482 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/skill-sweep.test.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { promoteReferenceSkill, sweepRunInstalledSkills, -} from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +} from '@agent/runner/sequence/orchestrator/orchestrator-runner'; function makeSkill(root: string, id: string, wizardMarked: boolean): string { const dir = path.join(root, id); diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts similarity index 99% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts index 79056bd33..f1227f562 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts @@ -33,7 +33,7 @@ import { consentSkipReason, offerSeededTask, TASK_NOTICE_TIMEOUT_MS, -} from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; +} from '@agent/runner/sequence/orchestrator/orchestrator-runner'; /** The answerer under test, standing where `getUI()` used to. */ const interaction = { diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts similarity index 98% rename from src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts rename to src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts index a8e5483ad..afbc48a15 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { resolveSkillVariantId } from '../orchestrator-runner'; import { Integration } from '@shared/constants'; -import { expandBundleEntry } from '@lib/wizard-tools'; -import type { SkillEntry } from '@lib/wizard-tools'; +import { expandBundleEntry } from '@agent/tools'; +import type { SkillEntry } from '@agent/tools'; // Pinned from the real built skill-menu.json, so this suite tests the actual cross-repo contract. const INTEGRATION_ENTRIES = [ diff --git a/src/lib/agent/runner/sequence/orchestrator/executor.ts b/src/agent/runner/sequence/orchestrator/executor.ts similarity index 100% rename from src/lib/agent/runner/sequence/orchestrator/executor.ts rename to src/agent/runner/sequence/orchestrator/executor.ts diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts similarity index 99% rename from src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts rename to src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index c68917390..7233f4b8f 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -22,19 +22,19 @@ import { writeFileSync, } from 'fs'; import * as path from 'path'; -import { OutroKind, type TaskNotice } from '@lib/agent/progress'; +import { OutroKind, type TaskNotice } from '@agent/progress'; import { POSTHOG_DOCS_URL, WIZARD_CONTACT_EMAIL } from '@shared/constants'; import { installSkillById, fetchSkillMenu, type SkillEntry, -} from '@lib/wizard-tools'; +} from '@agent/tools'; import { analytics } from '@utils/analytics'; import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; import { ErrorCodes, WizardError } from '@shared/errors'; -import type { AgentInteraction } from '@lib/agent/progress'; +import type { AgentInteraction } from '@agent/progress'; import type { AgentFailure, SequenceResult, @@ -62,7 +62,7 @@ import { drainQueue, RunTaskFatal, type RunTask } from './executor'; import { RunMetrics } from './run-metrics'; import { dependencyClosure, uncoveredBySink } from './queue-tools'; import { deferSeededTasks } from './seeded-deps'; -import { LONGER_ASK_TIMEOUT_MS } from '@lib/wizard-ask-bridge'; +import { LONGER_ASK_TIMEOUT_MS } from '@agent/wizard-ask-bridge'; import { shouldDisableAsk } from '../../shared/bootstrap'; import { agentRunTools, @@ -75,7 +75,7 @@ import { ASK_TOOL, type AgentPrompt, type OrchestratorPromptContext, -} from '@lib/agent/agent-prompt-loader'; +} from '@agent/agent-prompt-loader'; /** Docs page (`django.md`, `nuxt-js-3-6.md`) — steps start with a digit, agent artifacts (`SKILL.md`, `EXAMPLE*`, `COMMANDMENTS.md`) have uppercase. */ const isDocPage = (name: string): boolean => diff --git a/src/lib/agent/runner/sequence/orchestrator/queue-tools.ts b/src/agent/runner/sequence/orchestrator/queue-tools.ts similarity index 99% rename from src/lib/agent/runner/sequence/orchestrator/queue-tools.ts rename to src/agent/runner/sequence/orchestrator/queue-tools.ts index 2e2955823..7ef394f20 100644 --- a/src/lib/agent/runner/sequence/orchestrator/queue-tools.ts +++ b/src/agent/runner/sequence/orchestrator/queue-tools.ts @@ -8,10 +8,7 @@ */ import { z } from 'zod'; import { analytics } from '@utils/analytics'; -import { - isValidModel, - VALID_MODELS, -} from '@lib/agent/runner/switchboard/models'; +import { isValidModel, VALID_MODELS } from '@agent/runner/switchboard/models'; import { isNotNeededReason, NotNeededReason, diff --git a/src/lib/agent/runner/sequence/orchestrator/queue.ts b/src/agent/runner/sequence/orchestrator/queue.ts similarity index 100% rename from src/lib/agent/runner/sequence/orchestrator/queue.ts rename to src/agent/runner/sequence/orchestrator/queue.ts diff --git a/src/lib/agent/runner/sequence/orchestrator/run-metrics.ts b/src/agent/runner/sequence/orchestrator/run-metrics.ts similarity index 100% rename from src/lib/agent/runner/sequence/orchestrator/run-metrics.ts rename to src/agent/runner/sequence/orchestrator/run-metrics.ts diff --git a/src/lib/agent/runner/sequence/orchestrator/seeded-deps.ts b/src/agent/runner/sequence/orchestrator/seeded-deps.ts similarity index 100% rename from src/lib/agent/runner/sequence/orchestrator/seeded-deps.ts rename to src/agent/runner/sequence/orchestrator/seeded-deps.ts diff --git a/src/lib/agent/runner/shared/ask.ts b/src/agent/runner/shared/ask.ts similarity index 92% rename from src/lib/agent/runner/shared/ask.ts rename to src/agent/runner/shared/ask.ts index b7c6c1050..ffe178940 100644 --- a/src/lib/agent/runner/shared/ask.ts +++ b/src/agent/runner/shared/ask.ts @@ -12,8 +12,8 @@ import { createWizardAskBridge, type WizardAskBridge, -} from '@lib/wizard-ask-bridge'; -import type { AgentInteraction } from '@lib/agent/progress'; +} from '@agent/wizard-ask-bridge'; +import type { AgentInteraction } from '@agent/progress'; export function createAskBridge( interaction: AgentInteraction | undefined, diff --git a/src/lib/agent/runner/shared/bootstrap.ts b/src/agent/runner/shared/bootstrap.ts similarity index 97% rename from src/lib/agent/runner/shared/bootstrap.ts rename to src/agent/runner/shared/bootstrap.ts index d666daee5..43d8a570f 100644 --- a/src/lib/agent/runner/shared/bootstrap.ts +++ b/src/agent/runner/shared/bootstrap.ts @@ -8,8 +8,8 @@ * arrives already resolved in `RunConfig` and `RunInput`. */ -import { createTriageLLMProvider } from '@lib/agent/triage-provider'; -import { gatewayAuth } from '@lib/gateway-session'; +import { createTriageLLMProvider } from '@agent/triage-provider'; +import { gatewayAuth } from '@agent/gateway-session'; import { logToFile } from '@utils/debug'; import { CallType, IS_DEV } from '@shared/constants'; import { VERSION } from '@shared/version'; diff --git a/src/lib/agent/runner/shared/errors.ts b/src/agent/runner/shared/errors.ts similarity index 95% rename from src/lib/agent/runner/shared/errors.ts rename to src/agent/runner/shared/errors.ts index c5b6c27f9..d89d3c214 100644 --- a/src/lib/agent/runner/shared/errors.ts +++ b/src/agent/runner/shared/errors.ts @@ -2,7 +2,7 @@ * Shared error helpers for the runner pipeline. */ -import type { InstallSkillResult } from '@lib/wizard-tools'; +import type { InstallSkillResult } from '@agent/tools'; import { skillErrorCode } from '@shared/errors'; import { WizardError } from '@shared/errors'; import { RunOutcome, type AgentFailure, type SequenceResult } from './types'; diff --git a/src/lib/agent/runner/shared/progress-collector.ts b/src/agent/runner/shared/progress-collector.ts similarity index 99% rename from src/lib/agent/runner/shared/progress-collector.ts rename to src/agent/runner/shared/progress-collector.ts index 8bc63e7da..719603e63 100644 --- a/src/lib/agent/runner/shared/progress-collector.ts +++ b/src/agent/runner/shared/progress-collector.ts @@ -14,7 +14,7 @@ import type { AgentProgress, ProgressEmitter, SpinnerHandle, -} from '@lib/agent/progress'; +} from '@agent/progress'; import type { RunSnapshot } from './types'; export interface ProgressCollector { diff --git a/src/lib/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts similarity index 97% rename from src/lib/agent/runner/shared/types.ts rename to src/agent/runner/shared/types.ts index e5c7cdfe8..3f97d5f5d 100644 --- a/src/lib/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -12,14 +12,14 @@ import type { AdditionalFeature } from '@shared/constants'; import type { CloudRegion } from '@utils/types'; import type { Credentials } from '@shared/api'; -import type { OutroData, TaskNotice } from '@lib/agent/progress'; -import type { PromptContext } from '@lib/agent/agent-prompt'; +import type { OutroData, TaskNotice } from '@agent/progress'; +import type { PromptContext } from '@agent/agent-prompt'; import type { PackageManagerDetector } from '@utils/package-manager'; import type { ApiProject, ApiUser } from '@shared/api'; import type { Harness, Integration, Sequence } from '@shared/constants'; import type { ErrorCode } from '@shared/errors'; import type { LLMProvider } from '@posthog/warlock'; -import type { AgentInteraction, ProgressEmitter } from '@lib/agent/progress'; +import type { AgentInteraction, ProgressEmitter } from '@agent/progress'; import type { EffortLevel } from '../switchboard/models'; import type { SwitchboardCtx } from '../switchboard'; @@ -272,7 +272,7 @@ export interface TokenUsageTotals { /** What the agent reported, accumulated independently of any observer. */ export interface RunSnapshot { - tasks: import('@lib/agent/progress').TaskSnapshot[]; + tasks: import('@agent/progress').TaskSnapshot[]; statusMessages: string[]; stage?: string; usage: TokenUsageTotals; @@ -308,7 +308,7 @@ export type RunResult = ( export interface RunAgentOptions { /** Receives every progress event in emission order. Never awaited. */ - onProgress?: (event: import('@lib/agent/progress').AgentProgress) => void; + onProgress?: (event: import('@agent/progress').AgentProgress) => void; /** Answers the agent's questions. Absent → no ask bridge, notices declined. */ interaction?: AgentInteraction; } diff --git a/src/lib/agent/runner/switchboard/commandments.ts b/src/agent/runner/switchboard/commandments.ts similarity index 99% rename from src/lib/agent/runner/switchboard/commandments.ts rename to src/agent/runner/switchboard/commandments.ts index 5a71c51e7..5f709b1fb 100644 --- a/src/lib/agent/runner/switchboard/commandments.ts +++ b/src/agent/runner/switchboard/commandments.ts @@ -13,7 +13,7 @@ */ import { Harness, Sequence } from '@shared/constants'; -import { WIZARD_COMMANDMENTS } from '@lib/agent/commandments'; +import { WIZARD_COMMANDMENTS } from '@agent/commandments'; import { piRuntimeNotes, type RuntimeCaps } from '../harness/pi/runtime-notes'; // ── Sequence axis ─────────────────────────────────────────────────────── diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts b/src/agent/runner/switchboard/flags/__tests__/binding-cases.ts similarity index 94% rename from src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts rename to src/agent/runner/switchboard/flags/__tests__/binding-cases.ts index e3adfa25d..341a5d9a8 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/binding-cases.ts +++ b/src/agent/runner/switchboard/flags/__tests__/binding-cases.ts @@ -9,8 +9,8 @@ import { resolveBinding, type SwitchboardCtx, type SwitchboardTrace, -} from '@lib/agent/runner/switchboard'; -import type { EffortLevel } from '@lib/agent/runner/switchboard/models'; +} from '@agent/runner/switchboard'; +import type { EffortLevel } from '@agent/runner/switchboard/models'; /** The complete resolved binding — every axis stated, nothing implicit. */ export interface ExpectedBinding { diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/agent/runner/switchboard/flags/__tests__/flags.test.ts similarity index 98% rename from src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts rename to src/agent/runner/switchboard/flags/__tests__/flags.test.ts index 8d3360566..616ed2135 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -20,12 +20,12 @@ import { resolveBinding, resolveStageOverrides, type SwitchboardCtx, -} from '@lib/agent/runner/switchboard'; +} from '@agent/runner/switchboard'; import { ORCHESTRATOR_SEQUENCE_ROUTE, ORCHESTRATOR_HARNESS_ROUTE, -} from '@lib/agent/runner/switchboard/flags/orchestrator'; -import { SELF_DRIVING_EXPERIMENT } from '@lib/agent/runner/switchboard/flags/self-driving'; +} from '@agent/runner/switchboard/flags/orchestrator'; +import { SELF_DRIVING_EXPERIMENT } from '@agent/runner/switchboard/flags/self-driving'; import { runBindingCases } from './binding-cases'; const envState = vi.hoisted(() => ({ diff --git a/src/lib/agent/runner/switchboard/flags/index.ts b/src/agent/runner/switchboard/flags/index.ts similarity index 100% rename from src/lib/agent/runner/switchboard/flags/index.ts rename to src/agent/runner/switchboard/flags/index.ts diff --git a/src/lib/agent/runner/switchboard/flags/orchestrator.ts b/src/agent/runner/switchboard/flags/orchestrator.ts similarity index 100% rename from src/lib/agent/runner/switchboard/flags/orchestrator.ts rename to src/agent/runner/switchboard/flags/orchestrator.ts diff --git a/src/lib/agent/runner/switchboard/flags/schemes.ts b/src/agent/runner/switchboard/flags/schemes.ts similarity index 100% rename from src/lib/agent/runner/switchboard/flags/schemes.ts rename to src/agent/runner/switchboard/flags/schemes.ts diff --git a/src/lib/agent/runner/switchboard/flags/self-driving.ts b/src/agent/runner/switchboard/flags/self-driving.ts similarity index 100% rename from src/lib/agent/runner/switchboard/flags/self-driving.ts rename to src/agent/runner/switchboard/flags/self-driving.ts diff --git a/src/lib/agent/runner/switchboard/harness.ts b/src/agent/runner/switchboard/harness.ts similarity index 100% rename from src/lib/agent/runner/switchboard/harness.ts rename to src/agent/runner/switchboard/harness.ts diff --git a/src/lib/agent/runner/switchboard/index.ts b/src/agent/runner/switchboard/index.ts similarity index 100% rename from src/lib/agent/runner/switchboard/index.ts rename to src/agent/runner/switchboard/index.ts diff --git a/src/lib/agent/runner/switchboard/models.ts b/src/agent/runner/switchboard/models.ts similarity index 100% rename from src/lib/agent/runner/switchboard/models.ts rename to src/agent/runner/switchboard/models.ts diff --git a/src/lib/agent/runner/switchboard/sequence.ts b/src/agent/runner/switchboard/sequence.ts similarity index 100% rename from src/lib/agent/runner/switchboard/sequence.ts rename to src/agent/runner/switchboard/sequence.ts diff --git a/src/lib/safe-tools.ts b/src/agent/safe-tools.ts similarity index 100% rename from src/lib/safe-tools.ts rename to src/agent/safe-tools.ts diff --git a/src/lib/agent/signals.ts b/src/agent/signals.ts similarity index 100% rename from src/lib/agent/signals.ts rename to src/agent/signals.ts diff --git a/src/lib/agent/stored-login.ts b/src/agent/stored-login.ts similarity index 100% rename from src/lib/agent/stored-login.ts rename to src/agent/stored-login.ts diff --git a/src/lib/wizard-tools/__tests__/handoff.test.ts b/src/agent/tools/__tests__/handoff.test.ts similarity index 99% rename from src/lib/wizard-tools/__tests__/handoff.test.ts rename to src/agent/tools/__tests__/handoff.test.ts index c93b792c8..ff56b6df5 100644 --- a/src/lib/wizard-tools/__tests__/handoff.test.ts +++ b/src/agent/tools/__tests__/handoff.test.ts @@ -11,7 +11,7 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { AgentProgress } from '@lib/agent/progress'; +import type { AgentProgress } from '@agent/progress'; import { MAX_HANDOFF_TEXT_CHARS, publishHandoff } from '../handoff'; // The tool runs inside the agent, which has no UI; the handoff is a progress diff --git a/src/lib/wizard-tools/handoff.ts b/src/agent/tools/handoff.ts similarity index 99% rename from src/lib/wizard-tools/handoff.ts rename to src/agent/tools/handoff.ts index 67411da68..a64e8fa63 100644 --- a/src/lib/wizard-tools/handoff.ts +++ b/src/agent/tools/handoff.ts @@ -4,7 +4,7 @@ * It leaves the tool as a `handoff` progress event; the host projects it. */ -import type { ProgressEmitter } from '@lib/agent/progress'; +import type { ProgressEmitter } from '@agent/progress'; import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; import { runtimeEnv } from '@env'; diff --git a/src/lib/wizard-tools/index.ts b/src/agent/tools/index.ts similarity index 100% rename from src/lib/wizard-tools/index.ts rename to src/agent/tools/index.ts diff --git a/src/lib/wizard-tools/mcp.ts b/src/agent/tools/mcp.ts similarity index 99% rename from src/lib/wizard-tools/mcp.ts rename to src/agent/tools/mcp.ts index 339391d48..84c4b79a6 100644 --- a/src/lib/wizard-tools/mcp.ts +++ b/src/agent/tools/mcp.ts @@ -28,11 +28,11 @@ import { publishHandoff, } from './handoff'; import { createSecretVault, type SecretVault } from '@shared/secret-vault'; -import type { ProgressEmitter } from '@lib/agent/progress'; +import type { ProgressEmitter } from '@agent/progress'; import { buildOrchestratorTools, type OrchestratorToolsContext, -} from '../agent/runner/sequence/orchestrator/queue-tools'; +} from '@agent/runner/sequence/orchestrator/queue-tools'; import type { LLMProvider } from '@posthog/warlock'; import { DEFAULT_ASK_MAX_QUESTIONS, diff --git a/src/lib/wizard-tools/tools.ts b/src/agent/tools/tools.ts similarity index 99% rename from src/lib/wizard-tools/tools.ts rename to src/agent/tools/tools.ts index 100d8e7f7..c33ff5e80 100644 --- a/src/lib/wizard-tools/tools.ts +++ b/src/agent/tools/tools.ts @@ -20,7 +20,7 @@ import { type EnvKeyDefinition, type EnvKeyLocations, } from '@utils/env-scan'; -import { scanInstalledSkill } from '@lib/yara-hooks'; +import { scanInstalledSkill } from '@agent/yara-hooks'; import type { LLMProvider } from '@posthog/warlock'; import { writeJsonAtomic, makeMutex } from '@utils/atomic-ledger'; import { diff --git a/src/lib/agent/triage-provider.ts b/src/agent/triage-provider.ts similarity index 97% rename from src/lib/agent/triage-provider.ts rename to src/agent/triage-provider.ts index 092bccc53..426fab116 100644 --- a/src/lib/agent/triage-provider.ts +++ b/src/agent/triage-provider.ts @@ -10,11 +10,11 @@ import { logToFile } from '@utils/debug'; import { buildGatewayModel, gatewayApiFor, -} from '@lib/agent/runner/harness/pi/gateway'; +} from '@agent/runner/harness/pi/gateway'; import { modelCapabilities, triageModelFor, -} from '@lib/agent/runner/switchboard/models'; +} from '@agent/runner/switchboard/models'; import type { LLMProvider } from '@posthog/warlock'; const TRIAGE_MAX_TOKENS = 16_384; diff --git a/src/lib/wizard-ask-bridge.ts b/src/agent/wizard-ask-bridge.ts similarity index 98% rename from src/lib/wizard-ask-bridge.ts rename to src/agent/wizard-ask-bridge.ts index 290d18ee7..68de191e5 100644 --- a/src/lib/wizard-ask-bridge.ts +++ b/src/agent/wizard-ask-bridge.ts @@ -13,11 +13,7 @@ import { randomUUID } from 'crypto'; import { analytics } from '@utils/analytics'; -import type { - AskAnswers, - AskQuestion, - PendingQuestion, -} from './agent/progress'; +import type { AskAnswers, AskQuestion, PendingQuestion } from '@agent/progress'; export interface WizardAskRequest { questions: AskQuestion[]; diff --git a/src/lib/yara-hooks.ts b/src/agent/yara-hooks.ts similarity index 100% rename from src/lib/yara-hooks.ts rename to src/agent/yara-hooks.ts diff --git a/src/lib/yara-policy.ts b/src/agent/yara-policy.ts similarity index 100% rename from src/lib/yara-policy.ts rename to src/agent/yara-policy.ts diff --git a/src/commands/factories/family-command-factory.ts b/src/commands/factories/family-command-factory.ts index 23992e5f1..9ae0f164c 100644 --- a/src/commands/factories/family-command-factory.ts +++ b/src/commands/factories/family-command-factory.ts @@ -7,7 +7,7 @@ import { pickerChildrenToShow, } from '@lib/programs/dispatch-family'; import { getSkillsBaseUrl } from '@shared/constants'; -import { fetchSkillMenu } from '@lib/wizard-tools'; +import { fetchSkillMenu } from '@agent/tools'; import type { Command } from '../command'; import { createFamilyPickerDefault } from './family-picker'; diff --git a/src/commands/skill.ts b/src/commands/skill.ts index 01c21cc8b..c72fb93ec 100644 --- a/src/commands/skill.ts +++ b/src/commands/skill.ts @@ -1,7 +1,7 @@ import type { Arguments } from 'yargs'; import { getSkillsBaseUrl } from '@shared/constants'; -import { fetchSkillMenu, type CliEntry } from '@lib/wizard-tools'; +import { fetchSkillMenu, type CliEntry } from '@agent/tools'; import { analytics } from '@utils/analytics'; import { runSkillMode } from './basic-integration/skill'; diff --git a/src/lib/__tests__/wizard-spellbook.test.ts b/src/lib/__tests__/wizard-spellbook.test.ts index 02f017b0a..e23af172b 100644 --- a/src/lib/__tests__/wizard-spellbook.test.ts +++ b/src/lib/__tests__/wizard-spellbook.test.ts @@ -5,10 +5,10 @@ import { Integration } from '@shared/constants'; import type { ProgramConfig } from '../programs/program-step'; import { buildSession } from '../wizard-session'; import { writeWizardSpellbook } from '../wizard-spellbook'; -import { downloadSkill, fetchSkillMenu } from '../wizard-tools/tools'; +import { downloadSkill, fetchSkillMenu } from '@agent/tools/tools'; -vi.mock('../wizard-tools/tools', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent/tools/tools', async (importOriginal) => ({ + ...(await importOriginal()), fetchSkillMenu: vi.fn(), downloadSkill: vi.fn(), })); diff --git a/src/lib/detection/__tests__/agentic-progress.test.ts b/src/lib/detection/__tests__/agentic-progress.test.ts index 1f9db65c4..41e7f0761 100644 --- a/src/lib/detection/__tests__/agentic-progress.test.ts +++ b/src/lib/detection/__tests__/agentic-progress.test.ts @@ -2,7 +2,7 @@ import { detectProjectsWithAgent } from '../agentic'; import { initializeAgent, runAgent as executeAgent, -} from '@lib/agent/agent-interface'; +} from '@agent/agent-interface'; import { buildSession } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { getUI } from '@ui'; @@ -15,8 +15,8 @@ const ui = vi.hoisted(() => ({ pushStatus: vi.fn(), log: { error: vi.fn() }, })); -vi.mock('@lib/agent/agent-interface', async (original) => ({ - ...(await original()), +vi.mock('@agent/agent-interface', async (original) => ({ + ...(await original()), initializeAgent: vi.fn(), runAgent: vi.fn(), })); diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index 342eaf883..c2ce6a279 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -18,7 +18,7 @@ import { runAgent as executeAgent, buildRunTags, AgentSignals, -} from '@lib/agent/agent-interface'; +} from '@agent/agent-interface'; import { isAbsolute, resolve, sep } from 'path'; import { detectNodePackageManagers } from './package-manager.js'; import { CallType, getSkillsBaseUrl, HAIKU_MODEL } from '@shared/constants'; diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index 9af88da2b..1fdce1e41 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -1,7 +1,7 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; import { authenticate } from '@lib/programs/authenticate'; import { runProgramAgent } from '../run-agent-legacy'; -import { runAgent, RunOutcome, type RunResult } from '@lib/agent/runner'; +import { runAgent, RunOutcome, type RunResult } from '@agent/runner'; import { Harness, Sequence } from '@shared/constants'; import { buildSession, OutroKind } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; @@ -25,8 +25,8 @@ vi.mock('@utils/environment', async (original) => ({ ...(await original()), readEnvironment: () => ({}), })); -vi.mock('@lib/gateway-session', async (original) => ({ - ...(await original()), +vi.mock('@agent/gateway-session', async (original) => ({ + ...(await original()), configureGatewayFromCIEnvironment: vi.fn(), })); vi.mock('@lib/task-stream/index', () => ({ @@ -50,8 +50,8 @@ vi.mock('@utils/analytics', () => ({ }, sessionProperties: () => ({}), })); -vi.mock('@lib/agent/runner', async (original) => ({ - ...(await original()), +vi.mock('@agent/runner', async (original) => ({ + ...(await original()), runAgent: vi.fn(), })); vi.mock('@lib/programs/authenticate', () => ({ diff --git a/src/lib/programs/__tests__/self-driving-detect.test.ts b/src/lib/programs/__tests__/self-driving-detect.test.ts index b7ab13a60..a2ac4f929 100644 --- a/src/lib/programs/__tests__/self-driving-detect.test.ts +++ b/src/lib/programs/__tests__/self-driving-detect.test.ts @@ -23,7 +23,7 @@ import { type AgenticDetectionReport, } from '@lib/detection/agentic'; import { Integration } from '@shared/constants'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { buildSession } from '@lib/wizard-session'; import type { Mock } from 'vitest'; diff --git a/src/lib/programs/__tests__/self-driving-prompt.test.ts b/src/lib/programs/__tests__/self-driving-prompt.test.ts index fefc088c2..a2b886c5d 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 '@agent/agent-runner'; import { HostResolution } from '@shared/host-resolution'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/__tests__/warehouse-ask-timeout.test.ts b/src/lib/programs/__tests__/warehouse-ask-timeout.test.ts index c74c5f766..77850402d 100644 --- a/src/lib/programs/__tests__/warehouse-ask-timeout.test.ts +++ b/src/lib/programs/__tests__/warehouse-ask-timeout.test.ts @@ -22,7 +22,7 @@ import { warehouseSourceConfig } from '@lib/programs/warehouse-source/index'; import { LONGER_ASK_TIMEOUT_MS, DEFAULT_ASK_TIMEOUT_MS, -} from '@lib/wizard-ask-bridge'; +} from '@agent/wizard-ask-bridge'; function session(): WizardSession { return { installDir: '/tmp/app', frameworkContext: {} } as WizardSession; diff --git a/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts b/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts index 2d689c11c..65f338d87 100644 --- a/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts +++ b/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts @@ -6,7 +6,7 @@ import { webAnalyticsDoctorConfig, WEB_ANALYTICS_ABORT_CASES, } from '@lib/programs/web-analytics-doctor/index'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { buildSession } from '@lib/wizard-session'; function makeTmpDir(): string { diff --git a/src/lib/programs/agent-skill/index.ts b/src/lib/programs/agent-skill/index.ts index ace6ab19d..8ab764911 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 { AbortCase } from '@lib/agent/agent-runner'; +import type { AbortCase } from '@agent/agent-runner'; import type { ProgramRun } from '@lib/programs/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 c5e4d4c5e..37ee475b2 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 '@agent/agent-runner'; import { ErrorCodes } from '@shared/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 261fbaefc..110f09816 100644 --- a/src/lib/programs/audit/index.ts +++ b/src/lib/programs/audit/index.ts @@ -6,7 +6,7 @@ import type { ProgramStep, ProgramConfig } from '@lib/programs/program-step'; import type { ProgramRun } from '@lib/programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { headlessOption, regionOption } from '@lib/headless-mode'; import { AUDIT_ABORT_CASES } from './detect.js'; import { diff --git a/src/lib/programs/dispatch-family.ts b/src/lib/programs/dispatch-family.ts index ebb199ecd..e36831674 100644 --- a/src/lib/programs/dispatch-family.ts +++ b/src/lib/programs/dispatch-family.ts @@ -2,12 +2,12 @@ import type { Arguments } from 'yargs'; import { auditConfig } from '@lib/programs/audit/index'; import { AUDIT_CHECKS_FILE } from '@lib/programs/audit/types'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { agentSkillConfig } from '@lib/programs/program-registry'; import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; import type { ProgramConfig } from '@lib/programs/program-step'; import { getSkillsBaseUrl } from '@shared/constants'; -import { fetchSkillMenu, type CliEntry } from '@lib/wizard-tools'; +import { fetchSkillMenu, type CliEntry } from '@agent/tools'; import { analytics } from '@utils/analytics'; import { dispatchProgram } from '../../commands/factories/shared'; 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 8e67df613..15ff114af 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 '@agent/agent-runner'; import { ErrorCodes } from '@shared/errors'; /** 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..df2539095 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 '@agent/agent-interface'; import type { SkillVariant } from './detect.js'; export type SourceMapsUploadPromptParams = { diff --git a/src/lib/programs/events-audit/index.ts b/src/lib/programs/events-audit/index.ts index 4cfa397f0..850ae62af 100644 --- a/src/lib/programs/events-audit/index.ts +++ b/src/lib/programs/events-audit/index.ts @@ -4,7 +4,7 @@ import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { SPINNER_MESSAGE } from '@lib/framework-config'; import { isUsingTypeScript } from '@utils/setup-utils'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { EVENTS_AUDIT_PROGRAM } from './steps.js'; import { AUDIT_CHECKS_FILE, AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; import { seedAuditLedger } from '@lib/programs/audit/seed'; diff --git a/src/lib/programs/mcp-analytics/index.ts b/src/lib/programs/mcp-analytics/index.ts index b5e79e445..4b62d407b 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 '@agent/agent-runner'; import { ErrorCodes } from '@shared/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..2fc157746 100644 --- a/src/lib/programs/migration/index.ts +++ b/src/lib/programs/migration/index.ts @@ -1,6 +1,6 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { AbortCase } from '@lib/agent/agent-runner'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import type { AbortCase } from '@agent/agent-runner'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { MIGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/posthog-doctor/index.ts b/src/lib/programs/posthog-doctor/index.ts index 8ea70acf1..cf57eb9c7 100644 --- a/src/lib/programs/posthog-doctor/index.ts +++ b/src/lib/programs/posthog-doctor/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { POSTHOG_DOCTOR_PROGRAM } from './steps.js'; export const posthogDoctorConfig: ProgramConfig = { diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index ab8551e21..5a04cc46d 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -1,10 +1,10 @@ import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import type { ProgramRun } from '@lib/programs/program-run'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/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 '@agent/agent-interface'; import { DEFAULT_PACKAGE_INSTALLATION, SPINNER_MESSAGE, @@ -22,7 +22,7 @@ import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; import type { HostResolution } from '@shared/host-resolution'; import { getDetectedWarehouseSources } from '@lib/programs/warehouse-source/detect'; -import { shouldDisableAsk } from '@lib/agent/agent-runner'; +import { shouldDisableAsk } from '@agent/agent-runner'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; import { buildCodingAgentPrompt } from './handoff.js'; diff --git a/src/lib/programs/program-run.ts b/src/lib/programs/program-run.ts index 35a3ea90f..523d3bbbe 100644 --- a/src/lib/programs/program-run.ts +++ b/src/lib/programs/program-run.ts @@ -5,7 +5,7 @@ * agent `RunConfig.hooks`. */ -import type { AgentRunDefinition } from '@lib/agent/runner'; +import type { AgentRunDefinition } from '@agent/runner'; import type { Credentials, WizardSession } from '@lib/wizard-session'; export interface ProgramRun extends AgentRunDefinition { diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index 9c2fe78b3..637f162f5 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 '@agent/agent-runner'; import { Integration } from '@shared/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..7089e6e51 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 '@agent/agent-runner'; import { findPackageJsons } from '@lib/programs/shared/package-scanning'; export { diff --git a/src/lib/programs/revenue-analytics/index.ts b/src/lib/programs/revenue-analytics/index.ts index f75778bd1..9194f9e70 100644 --- a/src/lib/programs/revenue-analytics/index.ts +++ b/src/lib/programs/revenue-analytics/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import { WIZARD_TOOL_NAMES } from '@lib/wizard-tools'; +import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { REVENUE_ANALYTICS_PROGRAM } from './steps.js'; import { REVENUE_ABORT_CASES } from './detect.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index 703a4e519..f259533d6 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -24,17 +24,17 @@ import { type RunConfig, type RunInput, type SwitchboardCtx, -} from '@lib/agent/runner'; -import type { ProgramBinding } from '@lib/agent/runner/switchboard'; +} from '@agent/runner'; +import type { ProgramBinding } from '@agent/runner/switchboard'; import type { ProgramRun } from './program-run'; -import { buildRunTags } from '@lib/agent/agent-interface'; +import { buildRunTags } from '@agent/agent-interface'; import { backupAndFixClaudeSettings, checkAllSettingsConflicts, classifySettingsConflicts, restoreClaudeSettings, } from '@shared/claude-settings'; -import { flushScanReport } from '@lib/yara-hooks'; +import { flushScanReport } from '@agent/yara-hooks'; import { evaluateWizardReadiness, WizardReadiness, diff --git a/src/lib/programs/self-driving/detect.ts b/src/lib/programs/self-driving/detect.ts index e88aab1c3..080b4ce58 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 '@agent/agent-runner'; import { ErrorCodes } from '@shared/errors'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/self-driving/prompt.ts b/src/lib/programs/self-driving/prompt.ts index 5bb0a6af2..b79eeaaf9 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 '@agent/agent-interface'; +import type { PromptContext } from '@agent/agent-runner'; 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..c9cfa9673 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 '@agent/agent-runner'; 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 4b31f3851..7b5e99c23 100644 --- a/src/lib/programs/warehouse-source/index.ts +++ b/src/lib/programs/warehouse-source/index.ts @@ -1,7 +1,7 @@ import type { ProgramConfig } from '@lib/programs/program-step'; import type { ProgramRun } from '@lib/programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; -import { LONGER_ASK_TIMEOUT_MS } from '@lib/wizard-ask-bridge'; +import { LONGER_ASK_TIMEOUT_MS } from '@agent/wizard-ask-bridge'; import { WAREHOUSE_SOURCE_PROGRAM } from './steps.js'; import { WAREHOUSE_ABORT_CASES, diff --git a/src/lib/programs/web-analytics-doctor/detect.ts b/src/lib/programs/web-analytics-doctor/detect.ts index f1ec56484..30ab8ce01 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 '@agent/agent-runner'; import { ErrorCodes } from '@shared/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 d09fd9249..dfa32f9c7 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -243,7 +243,7 @@ export function runNonInteractive( try { if (mode === 'ci') { const { configureGatewayFromCIEnvironment } = await import( - '@lib/gateway-session' + '@agent/gateway-session' ); configureGatewayFromCIEnvironment( Number(session.projectId), diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 52959c8e9..af7fdd77e 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -31,7 +31,7 @@ import { type OutroData, type PendingQuestion, type TaskNotice, -} from './agent/progress'; +} from '@agent/progress'; // These shapes moved to their owners; re-exported so every session reader // keeps its import path. `Credentials` sits with the API types, the diff --git a/src/lib/wizard-spellbook.ts b/src/lib/wizard-spellbook.ts index 9477f23f0..894ae1f6e 100644 --- a/src/lib/wizard-spellbook.ts +++ b/src/lib/wizard-spellbook.ts @@ -8,7 +8,7 @@ import { fetchSkillMenu, type SkillEntry, type SkillMenu, -} from './wizard-tools/tools'; +} from '@agent/tools/tools'; export type WizardSpellbook = { path: string; skillsIncluded: boolean }; type SpellbookSession = Pick< diff --git a/src/shared/claude-settings.ts b/src/shared/claude-settings.ts index 6765dc43b..da4873eec 100644 --- a/src/shared/claude-settings.ts +++ b/src/shared/claude-settings.ts @@ -15,7 +15,7 @@ import { registerCleanup } from '@utils/wizard-abort'; import { BLOCKED_AGENT_ENV_KEYS, BLOCKED_AGENT_ENV_PATTERNS, -} from '@lib/agent/agent-env-isolation'; +} from '@agent/agent-env-isolation'; /** The file names Claude Code accepts for project settings, in priority order. */ const SETTINGS_FILENAMES = ['settings.json', 'settings'] as const; diff --git a/src/shared/errors/__tests__/run-failure.test.ts b/src/shared/errors/__tests__/run-failure.test.ts index 3d9c30091..9682327b6 100644 --- a/src/shared/errors/__tests__/run-failure.test.ts +++ b/src/shared/errors/__tests__/run-failure.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { classifyRunFailure } from '../run-failure'; import { ErrorCodes } from '../codes'; import { WizardError } from '@utils/wizard-abort'; -import { GatewayMintRefused } from '@lib/gateway-session'; +import { GatewayMintRefused } from '@agent/gateway-session'; vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn(), captureException: vi.fn() }, diff --git a/src/shared/errors/agent-map.ts b/src/shared/errors/agent-map.ts index d5b9143e6..c1160d14b 100644 --- a/src/shared/errors/agent-map.ts +++ b/src/shared/errors/agent-map.ts @@ -1,4 +1,4 @@ -import { AgentErrorType } from '@lib/agent/signals'; +import { AgentErrorType } from '@agent/signals'; import { ErrorCodes, type ErrorCode } from './codes'; export const AGENT_ERROR_CODE: Record = { diff --git a/src/shared/errors/skill-map.ts b/src/shared/errors/skill-map.ts index 1bca38eef..50f9851fb 100644 --- a/src/shared/errors/skill-map.ts +++ b/src/shared/errors/skill-map.ts @@ -1,5 +1,5 @@ import { ErrorCodes, type ErrorCode } from './codes'; -import type { InstallSkillResult } from '@lib/wizard-tools'; +import type { InstallSkillResult } from '@agent/tools'; const SKILL_CODES: Record< Exclude, diff --git a/src/shared/utils/__tests__/environment.test.ts b/src/shared/utils/__tests__/environment.test.ts index 7938c399c..0830d12c5 100644 --- a/src/shared/utils/__tests__/environment.test.ts +++ b/src/shared/utils/__tests__/environment.test.ts @@ -10,7 +10,7 @@ import { readEnvironment } from '@utils/environment'; import { buildSession } from '@lib/wizard-session'; -import { shouldDisableAsk } from '@lib/agent/agent-runner'; +import { shouldDisableAsk } from '@agent/agent-runner'; /** Every var this file sets, cleared between cases. */ const TOUCHED = [ diff --git a/src/ui/__tests__/agent-progress.test.ts b/src/ui/__tests__/agent-progress.test.ts index afc4cb788..b6d20e399 100644 --- a/src/ui/__tests__/agent-progress.test.ts +++ b/src/ui/__tests__/agent-progress.test.ts @@ -3,7 +3,7 @@ vi.mock('@utils/debug'); import { createUiReducer, uiInteraction } from '../agent-progress'; import { LoggingUI } from '../logging-ui'; -import type { AgentProgress } from '@lib/agent/progress'; +import type { AgentProgress } from '@agent/progress'; import { OutroKind } from '@lib/wizard-session'; it('projects every progress event onto the matching UI call, in order', () => { diff --git a/src/ui/agent-progress.ts b/src/ui/agent-progress.ts index c5c406f65..a28dcbf27 100644 --- a/src/ui/agent-progress.ts +++ b/src/ui/agent-progress.ts @@ -1,5 +1,5 @@ import type { WizardUI, SpinnerHandle } from './wizard-ui'; -import type { AgentInteraction, AgentProgress } from '@lib/agent/progress'; +import type { AgentInteraction, AgentProgress } from '@agent/progress'; // ── Progress → WizardUI, one call per event ─────────────────────────── diff --git a/src/ui/tui/__tests__/frames.test.tsx b/src/ui/tui/__tests__/frames.test.tsx index 33f281f11..c24a782af 100644 --- a/src/ui/tui/__tests__/frames.test.tsx +++ b/src/ui/tui/__tests__/frames.test.tsx @@ -50,7 +50,7 @@ vi.mock('@shared/api', async (actual) => ({ fetchSlackConnected: vi.fn(pending), fetchGithubConnected: vi.fn(pending), })); -vi.mock('@lib/wizard-tools', async (actual) => ({ +vi.mock('@agent/tools', async (actual) => ({ ...(await actual>()), fetchSkillMenu: vi.fn(pending), downloadSkill: vi.fn(pending), diff --git a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx index da8161d06..e240e36dd 100644 --- a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx +++ b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx @@ -66,8 +66,8 @@ vi.mock('@shared/api', async (importOriginal) => ({ fetchSlackConnected: vi.fn().mockResolvedValue(false), fetchUserData: vi.fn(() => new Promise(() => undefined)), })); -vi.mock('@lib/wizard-tools', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent/tools', async (importOriginal) => ({ + ...(await importOriginal()), fetchSkillMenu: vi.fn(() => new Promise(() => undefined)), })); vi.mock('@utils/setup-utils', async (importOriginal) => ({ diff --git a/src/ui/tui/__tests__/skill-entry.test.ts b/src/ui/tui/__tests__/skill-entry.test.ts index 1adece346..09f5d43a0 100644 --- a/src/ui/tui/__tests__/skill-entry.test.ts +++ b/src/ui/tui/__tests__/skill-entry.test.ts @@ -1,5 +1,5 @@ import { resolveSkillEntry } from '@ui/tui/screens/SkillSourceInfo'; -import type { SkillEntry } from '@lib/wizard-tools'; +import type { SkillEntry } from '@agent/tools'; const entry = (id: string): SkillEntry => ({ id, downloadUrl: `https://example.com/${id}.tar.gz` } as SkillEntry); diff --git a/src/ui/tui/screens/SkillSourceInfo.tsx b/src/ui/tui/screens/SkillSourceInfo.tsx index 5edd8f443..7805f433c 100644 --- a/src/ui/tui/screens/SkillSourceInfo.tsx +++ b/src/ui/tui/screens/SkillSourceInfo.tsx @@ -14,7 +14,7 @@ import { Box, Text } from 'ink'; import { useEffect, useState } from 'react'; -import { fetchSkillMenu, type SkillEntry } from '@lib/wizard-tools'; +import { fetchSkillMenu, type SkillEntry } from '@agent/tools'; import { CONTEXT_MILL_RELEASES_URL, getSkillsBaseUrl } from '@shared/constants'; /** diff --git a/src/ui/tui/screens/health/HealthCheckScreen.tsx b/src/ui/tui/screens/health/HealthCheckScreen.tsx index 4698cd178..c19e6f5af 100644 --- a/src/ui/tui/screens/health/HealthCheckScreen.tsx +++ b/src/ui/tui/screens/health/HealthCheckScreen.tsx @@ -24,7 +24,7 @@ import { import { ServiceHealthStatus } from '@shared/health-checks/types'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; -import { fetchSkillMenu, downloadSkill } from '@lib/wizard-tools'; +import { fetchSkillMenu, downloadSkill } from '@agent/tools'; import { GITHUB_SKILLS_BASE_URL } from '@shared/constants'; import { useDismissOnAnyKey } from '@ui/tui/hooks/useDismissOnAnyKey'; diff --git a/src/ui/tui/services/mcp-suggested-prompts-services.ts b/src/ui/tui/services/mcp-suggested-prompts-services.ts index 5561a6da1..8651f8dc9 100644 --- a/src/ui/tui/services/mcp-suggested-prompts-services.ts +++ b/src/ui/tui/services/mcp-suggested-prompts-services.ts @@ -23,7 +23,7 @@ import { seedDemoEvents as runSeed } from '@lib/mcp-seed-events'; // The streamed event shape is the agent's; re-exported so the screen and the // playground keep their import path. -import type { AgentChunk } from '@lib/agent/mcp-prompt-streaming'; +import type { AgentChunk } from '@agent/mcp-prompt-streaming'; export type { AgentChunk }; export interface McpSuggestedPromptsServices { @@ -161,8 +161,6 @@ async function* runProductionPromptStreaming(args: { // Defer the SDK import to call time — the playground never hits // this path (it overrides the whole service object), so demo // sessions don't pay the SDK load cost. - const { runMcpPromptViaSdk } = await import( - '@lib/agent/mcp-prompt-streaming' - ); + const { runMcpPromptViaSdk } = await import('@agent/mcp-prompt-streaming'); yield* runMcpPromptViaSdk(args); } diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index 9d00d469f..f99802402 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -34,7 +34,7 @@ import type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta, -} from '@lib/agent/progress'; +} from '@agent/progress'; export type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta }; export interface WizardUI { diff --git a/tsconfig.build.json b/tsconfig.build.json index 70420b6ae..f4c4ef2d9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -20,6 +20,7 @@ "paths": { "@env": ["./src/env.ts"], "@shared/*": ["./src/shared/*"], + "@agent/*": ["./src/agent/*"], "@lib/*": ["./src/lib/*"], "@e2e-harness/*": ["./e2e-harness/*"], "@utils/*": ["./src/shared/utils/*"], diff --git a/tsconfig.json b/tsconfig.json index 57db83830..b3b6d448f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,6 @@ "bin.ts", "tsdown.config.ts", "vitest.config.ts", - "src/lib/**/*", "spec/**/*", "src/**/*", "test/**/*", diff --git a/vitest.config.ts b/vitest.config.ts index 9107d17e9..14d4149f8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -32,14 +32,7 @@ function resolveTsForJs(): Plugin { // Per-surface Vitest projects keyed by today's directories. Each project runs // alone with `vitest run --project `; `vitest run` runs them all. const TESTS = '__tests__/**/*.{js,jsx,ts,tsx}'; -const AGENT_TESTS = [ - `src/lib/agent/**/${TESTS}`, - `src/lib/middleware/**/${TESTS}`, - 'src/lib/__tests__/agent-*.test.ts', - 'src/lib/__tests__/gateway-session.test.ts', - 'src/lib/__tests__/wizard-can-use-tool.test.ts', - 'src/lib/__tests__/yara-*.test.ts', -]; +const AGENT_TESTS = [`src/agent/**/${TESTS}`]; const TUI_TESTS = [`src/ui/tui/**/${TESTS}`]; const CLI_TESTS = [ `src/commands/**/${TESTS}`, @@ -86,6 +79,7 @@ export default defineConfig({ }, { find: /^ink$/, replacement: r('__mocks__/ink.ts') }, { find: /^@shared\/(.*)$/, replacement: `${r('src/shared')}/$1` }, + { find: /^@agent\/(.*)$/, replacement: `${r('src/agent')}/$1` }, // Path aliases — mirror tsconfig `paths`. { find: /^@env$/, replacement: r('src/env.ts') }, { find: /^@lib\/(.*)$/, replacement: `${r('src/lib')}/$1` }, From 273cd0eae88d56562db8f0338ad487017652294b Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 00:25:50 -0400 Subject: [PATCH 24/61] docs: update agent and shared source paths Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../skills/adding-framework-support/SKILL.md | 6 +-- .claude/skills/adding-skill-program/SKILL.md | 10 ++-- .claude/skills/exploring-the-wizard/SKILL.md | 2 +- .claude/skills/ink-tui/references/PATTERNS.md | 2 +- .../ink-tui/references/TERMINAL-COMPAT.md | 2 +- .claude/skills/wizard-development/SKILL.md | 18 ++++---- .../references/ARCHITECTURE.md | 46 +++++++++---------- .github/CODEOWNERS | 2 +- README.md | 24 +++++----- docs/benchmarking.md | 2 +- docs/error-catalog.md | 10 ++-- docs/local-dev.md | 2 +- docs/runbooks/aio-capture.md | 2 +- docs/runbooks/warlock-kill-switch.md | 10 ++-- scripts/smoke-test.sh | 2 +- .../programs/__tests__/mcp-analytics.test.ts | 2 +- src/lib/programs/self-driving/ARCHITECTURE.md | 18 ++++---- src/lib/wizard-session.ts | 4 +- src/shared/constants.ts | 4 +- src/shared/health-checks/testme.md | 2 +- 20 files changed, 86 insertions(+), 84 deletions(-) diff --git a/.claude/skills/adding-framework-support/SKILL.md b/.claude/skills/adding-framework-support/SKILL.md index a7ec93fc1..671b0ad24 100644 --- a/.claude/skills/adding-framework-support/SKILL.md +++ b/.claude/skills/adding-framework-support/SKILL.md @@ -25,7 +25,7 @@ example under [src/frameworks](../../../src/frameworks/). Framework-specific detection, context, environment conventions, and UI metadata belong here. Integration instructions and examples belong in context-mill. -1. Add the integration to [Integration](../../../src/lib/constants.ts). Its +1. Add the integration to [Integration](../../../src/shared/constants.ts). Its order controls first-match detection and the framework picker. Keep specific frameworks before language fallbacks and generic Node last; preserve the overlap rules in the @@ -59,7 +59,7 @@ itself. | [Laravel](../../../src/frameworks/laravel/) | Composer and framework-specific filesystem signals | | [Rails](../../../src/frameworks/rails/) | Gemfile detection and Ruby conventions | -Use [bounded filesystem helpers](../../../src/utils/bounded-fs.ts) for project +Use [bounded filesystem helpers](../../../src/shared/utils/bounded-fs.ts) for project scans and reads. They bound traversal and skip dependency/build directories; add framework-specific exclusions with `extraIgnore`. Keep complex parsers and detectors beside the config so they can be checked independently. @@ -71,7 +71,7 @@ matching integration reference and task-skill variants for the framework. A registry entry alone does not provide integration knowledge. The orchestrator resolves framework variants from the skill menu and rejects missing task variants; see the -[orchestrator runner](../../../src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts). +[orchestrator runner](../../../src/agent/runner/sequence/orchestrator/orchestrator-runner.ts). Keep project-specific facts in configuration and reusable integration guidance in that content. Model IDs, reasoning efforts, and gateway-required system diff --git a/.claude/skills/adding-skill-program/SKILL.md b/.claude/skills/adding-skill-program/SKILL.md index 883d63108..60c3f8fb3 100644 --- a/.claude/skills/adding-skill-program/SKILL.md +++ b/.claude/skills/adding-skill-program/SKILL.md @@ -21,7 +21,7 @@ guide owns the fallback criteria and gateway model/effort/system-prompt contract. These are contribution defaults. Current runtime -[bindings](../../../src/lib/agent/runner/switchboard/index.ts) still default +[bindings](../../../src/agent/runner/switchboard/index.ts) still default many programs to Anthropic plus linear; documentation changes do not migrate them. @@ -56,14 +56,14 @@ changing execution behavior. agent work. 2. Supply the flow's seed and task prompts in context-mill, including the task dependencies and applicable skill variants. The - [orchestrator](../../../src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts) + [orchestrator](../../../src/agent/runner/sequence/orchestrator/orchestrator-runner.ts) loads `agentFlow ?? id`, requires a seed prompt, and checks task-skill variants before running. `run.skillId` alone does not define this flow. 3. Register the config in [PROGRAM_REGISTRY](../../../src/lib/programs/program-registry.ts) and add its Pi/orchestrator entry to - [PROGRAM_BINDINGS](../../../src/lib/agent/runner/switchboard/index.ts). - [Existing binding checks](../../../src/lib/agent/runner/__tests__/switchboard.test.ts) + [PROGRAM_BINDINGS](../../../src/agent/runner/switchboard/index.ts). + [Existing binding checks](../../../src/agent/runner/__tests__/switchboard.test.ts) enforce coverage; `ProgramId` currently widens to `string`. 4. For a standalone native command, create a command module with [nativeCommandFactory](../../../src/commands/factories/native-command-factory.ts) @@ -93,7 +93,7 @@ config directly and adds prerequisite detection. `ProgramRun.customPrompt`, `abortCases`, `postRun`, and `buildOutroData` are consumed by the -[linear sequence](../../../src/lib/agent/runner/sequence/linear.ts). `postRun` +[linear sequence](../../../src/agent/runner/sequence/linear.ts). `postRun` runs after success; `buildOutroData` receives session and credentials, with host information inside credentials. The orchestrator currently uses its own task prompts, failure handling, and outro, and does not invoke those hooks. Check diff --git a/.claude/skills/exploring-the-wizard/SKILL.md b/.claude/skills/exploring-the-wizard/SKILL.md index 50dbc7160..2dc754f74 100644 --- a/.claude/skills/exploring-the-wizard/SKILL.md +++ b/.claude/skills/exploring-the-wizard/SKILL.md @@ -114,7 +114,7 @@ MCP snapshots are plain `.txt`; the CI snapshot route writes colored `.ans` frames. Keep the screen path and failure evidence with the snapshots. Do not stop a progressing run at an invented turn count: the MCP exposes no turn limit; Pi's continuation and tool-call guards are described in its -[harness README](../../../src/lib/agent/runner/harness/pi/README.md). +[harness README](../../../src/agent/runner/harness/pi/README.md). ## Sweep the workbench diff --git a/.claude/skills/ink-tui/references/PATTERNS.md b/.claude/skills/ink-tui/references/PATTERNS.md index 3f530e742..1db9a4c03 100644 --- a/.claude/skills/ink-tui/references/PATTERNS.md +++ b/.claude/skills/ink-tui/references/PATTERNS.md @@ -48,6 +48,6 @@ layout must update on terminal resize; reading `useStdout` alone does not subscribe. Account for the shell's title, hints, tabs, status expansion, and optional HUD rather than allocating the full terminal height to screen content. -Write diagnostics through [logToFile](../../../../src/utils/debug.ts), which +Write diagnostics through [logToFile](../../../../src/shared/utils/debug.ts), which uses the configured wizard log destination. Avoid introducing a separate hardcoded temporary log or writing debug lines into the live TUI. diff --git a/.claude/skills/ink-tui/references/TERMINAL-COMPAT.md b/.claude/skills/ink-tui/references/TERMINAL-COMPAT.md index f8b30a438..1b941103a 100644 --- a/.claude/skills/ink-tui/references/TERMINAL-COMPAT.md +++ b/.claude/skills/ink-tui/references/TERMINAL-COMPAT.md @@ -38,7 +38,7 @@ Mode selection belongs to command/runner entry points. The [default integration command](../../../../src/commands/basic-integration/index.ts) routes explicit CI/headless requests separately and rejects a detected noninteractive environment for the ordinary interactive path. -[Environment detection](../../../../src/utils/environment.ts) checks +[Environment detection](../../../../src/shared/utils/environment.ts) checks stdout/stderr TTY state outside development mode; checking stdin alone does not describe this command's behavior. diff --git a/.claude/skills/wizard-development/SKILL.md b/.claude/skills/wizard-development/SKILL.md index 3ee4f261d..cde67b09b 100644 --- a/.claude/skills/wizard-development/SKILL.md +++ b/.claude/skills/wizard-development/SKILL.md @@ -22,8 +22,8 @@ infrastructure should consume those boundaries. | Framework detection, context, env conventions | [FrameworkConfig](../../../src/lib/framework-config.ts) and [framework configs](../../../src/frameworks/) | | Integration instructions and orchestrator flows/tasks | [context-mill](https://github.com/PostHog/context-mill) | | Programs, steps, prerequisites and outcomes | [programs](../../../src/lib/programs/) | -| Sequence, harness, model and effort selection | [switchboard](../../../src/lib/agent/runner/switchboard/) | -| Local tool permissions and scanner adapters | [agent-interface](../../../src/lib/agent/agent-interface.ts), [YARA hooks](../../../src/lib/yara-hooks.ts), [Pi security](../../../src/lib/agent/runner/harness/pi/security.ts) | +| Sequence, harness, model and effort selection | [switchboard](../../../src/agent/runner/switchboard/) | +| Local tool permissions and scanner adapters | [agent-interface](../../../src/agent/agent-interface.ts), [YARA hooks](../../../src/agent/yara-hooks.ts), [Pi security](../../../src/agent/runner/harness/pi/security.ts) | | Scanner rules | [warlock](https://github.com/PostHog/warlock) | | Token admission and budgets | [PostHog mint endpoint](https://github.com/PostHog/posthog/blob/master/posthog/llm/wizard_gateway_token.py) and [ai-gateway](https://github.com/PostHog/ai-gateway) | | Screen resolution and rendering | [TUI](../../../src/ui/tui/) through [WizardUI](../../../src/ui/wizard-ui.ts) | @@ -43,7 +43,7 @@ infrastructure should consume those boundaries. new Anthropic models. Existing routing has not all migrated: -[DEFAULT_BINDING](../../../src/lib/agent/runner/switchboard/index.ts) still +[DEFAULT_BINDING](../../../src/agent/runner/switchboard/index.ts) still selects Anthropic + linear, with per-program and flag overrides. Set new bindings explicitly. Migrating an existing program requires checking its flow, tasks, and lifecycle hooks; changing the default constant alone is insufficient. @@ -52,8 +52,8 @@ Both harnesses implement `run` and `runTask`. Adding a model or effort is a cross-repository change: 1. Define the Wizard model ID and capabilities in - [constants](../../../src/lib/constants.ts) and - [models](../../../src/lib/agent/runner/switchboard/models.ts); select it + [constants](../../../src/shared/constants.ts) and + [models](../../../src/agent/runner/switchboard/models.ts); select it through the switchboard or supported context-mill stage metadata. 2. Check the PostHog mint's `WIZARD_MODEL_ALLOWLIST` and allowed efforts. The token's policy is enforced by the gateway; a local constant or CLI override @@ -65,7 +65,7 @@ Adding a model or effort is a cross-repository change: 4. Verify the chosen harness transport and effective effort with the same admission policy used by the target environment. -Local [commandments](../../../src/lib/agent/runner/switchboard/commandments.ts) +Local [commandments](../../../src/agent/runner/switchboard/commandments.ts) provide runtime and tool guidance. They do not replace the gateway's required safety prompt. Keep gateway policy owned there rather than copying it into skills. Do not infer per-model effort authorization merely from the local @@ -94,7 +94,7 @@ only when it gives a real owner a smaller, reusable boundary. ## Lifecycle and security -[runner/index.ts](../../../src/lib/agent/runner/index.ts) bootstraps, resolves a +[runner/index.ts](../../../src/agent/runner/index.ts) bootstraps, resolves a binding, and dispatches to a sequence. `agent-runner.ts` is a compatibility re-export, not the implementation. Sequences own their lifecycle; harnesses own SDK calls. `ProgramRun.postRun`, `buildOutroData`, `customPrompt`, and @@ -109,10 +109,10 @@ than assuming every rejection terminates the run. New scanner rules belong in warlock; changes to how a match is handled belong in Wizard. User-provided secrets should travel through -[secret-vault](../../../src/lib/secret-vault.ts) references. Tool +[secret-vault](../../../src/shared/secret-vault.ts) references. Tool implementations resolve values host-side where they are used; they must not return the raw value to the model. Inspect the -[wizard-tools](../../../src/lib/wizard-tools/) implementations and the Pi +[wizard-tools](../../../src/agent/tools/) implementations and the Pi adapter when extending this shared tool surface. ## Verification and maintenance diff --git a/.claude/skills/wizard-development/references/ARCHITECTURE.md b/.claude/skills/wizard-development/references/ARCHITECTURE.md index bb99255f3..c29badcf2 100644 --- a/.claude/skills/wizard-development/references/ARCHITECTURE.md +++ b/.claude/skills/wizard-development/references/ARCHITECTURE.md @@ -16,19 +16,19 @@ readiness hooks, and traverse steps and gates. detection in `onReady`. Noninteractive execution has its own lifecycle in [run-non-interactive.ts](../../../../src/lib/runners/run-non-interactive.ts). -[runner/index.ts](../../../../src/lib/agent/runner/index.ts) resolves a +[runner/index.ts](../../../../src/agent/runner/index.ts) resolves a program's `run` definition, calls shared bootstrap, selects a binding, dispatches the sequence, and flushes the scanner report on cleanup. The old -[agent-runner.ts](../../../../src/lib/agent/agent-runner.ts) is a compatibility +[agent-runner.ts](../../../../src/agent/agent-runner.ts) is a compatibility export. | Layer | Source and responsibility | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Bootstrap | [shared/bootstrap.ts](../../../../src/lib/agent/runner/shared/bootstrap.ts): shared health/settings/auth/flag and MCP setup | -| Switchboard | [switchboard/index.ts](../../../../src/lib/agent/runner/switchboard/index.ts): resolve sequence, harness, model and effort override | -| Linear sequence | [sequence/linear.ts](../../../../src/lib/agent/runner/sequence/linear.ts): one conversation, skill/prompt assembly, post-run hooks and outro | -| Orchestrator | [orchestrator-runner.ts](../../../../src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts): seed plan, task queue, focused conversations, handoffs and completion | -| Harness | [harness/types.ts](../../../../src/lib/agent/runner/harness/types.ts): SDK boundary, implemented by Pi and Anthropic | +| Bootstrap | [shared/bootstrap.ts](../../../../src/agent/runner/shared/bootstrap.ts): shared health/settings/auth/flag and MCP setup | +| Switchboard | [switchboard/index.ts](../../../../src/agent/runner/switchboard/index.ts): resolve sequence, harness, model and effort override | +| Linear sequence | [sequence/linear.ts](../../../../src/agent/runner/sequence/linear.ts): one conversation, skill/prompt assembly, post-run hooks and outro | +| Orchestrator | [orchestrator-runner.ts](../../../../src/agent/runner/sequence/orchestrator/orchestrator-runner.ts): seed plan, task queue, focused conversations, handoffs and completion | +| Harness | [harness/types.ts](../../../../src/agent/runner/harness/types.ts): SDK boundary, implemented by Pi and Anthropic | The contribution policy is [Pi by default, orchestration preferred](../SKILL.md#execution-policy-and-model-admission). @@ -40,7 +40,7 @@ route. ### Configuration hooks -Read [ProgramRun](../../../../src/lib/agent/runner/shared/types.ts) for exact +Read [ProgramRun](../../../../src/agent/runner/shared/types.ts) for exact signatures. | Surface | Current consumer | @@ -70,25 +70,25 @@ binding and stamping a trace of the selected precedence rungs. - Sequence: composed linear clamp, development CLI override, `runTask` capability clamp, program flag route, sequence experiment, binding/default. -[Harness](../../../../src/lib/agent/runner/switchboard/harness.ts) and -[sequence](../../../../src/lib/agent/runner/switchboard/sequence.ts) contain the +[Harness](../../../../src/agent/runner/switchboard/harness.ts) and +[sequence](../../../../src/agent/runner/switchboard/sequence.ts) contain the exact chains. Published builds omit CLI overrides. `RUN_SURFACE` can disable harness experiments; static bindings and harness capabilities also affect resolution. Composed sub-runs remain linear even when a CLI override requests orchestration. Effort resolves in two stages: the binding supplies an override, then -[modelCapabilities](../../../../src/lib/agent/runner/switchboard/models.ts) +[modelCapabilities](../../../../src/agent/runner/switchboard/models.ts) applies capabilities and defaults. All selected models and efforts must also be admitted by the minted token and gateway. Local routing cannot bypass that external policy; see the [model admission checklist](../SKILL.md#execution-policy-and-model-admission). Flags belong in -[switchboard/flags](../../../../src/lib/agent/runner/switchboard/flags/). +[switchboard/flags](../../../../src/agent/runner/switchboard/flags/). Experiments declare their program scope; malformed payloads yield no experiment route. Reuse the -[switchboard tests](../../../../src/lib/agent/runner/__tests__/switchboard.test.ts) +[switchboard tests](../../../../src/agent/runner/__tests__/switchboard.test.ts) and experiment tests to check full bindings and isolation of unrelated programs. Do not add a second flag-reading path inside a harness or sequence. @@ -99,17 +99,17 @@ Wizard's local tool boundary separately restricts operations on the user's project. Local commandments provide model guidance; they are not an enforcement mechanism. -- [agent-interface.ts](../../../../src/lib/agent/agent-interface.ts) configures +- [agent-interface.ts](../../../../src/agent/agent-interface.ts) configures the Anthropic SDK's tool permissions, sandbox, and gateway transport. -- [yara-hooks.ts](../../../../src/lib/yara-hooks.ts) adapts warlock scans to SDK +- [yara-hooks.ts](../../../../src/agent/yara-hooks.ts) adapts warlock scans to SDK tool hooks. -- [Pi security](../../../../src/lib/agent/runner/harness/pi/security.ts) adapts +- [Pi security](../../../../src/agent/runner/harness/pi/security.ts) adapts shared permissions and scanning to Pi tool-call/result events, including blocking, violation latching, and tool-call limits. -- [Pi harness](../../../../src/lib/agent/runner/harness/pi/) explicitly supplies +- [Pi harness](../../../../src/agent/runner/harness/pi/) explicitly supplies tools, scrubs shell environments, and disables project-controlled extensions/context loading. -- [triage-provider.ts](../../../../src/lib/agent/triage-provider.ts) supplies +- [triage-provider.ts](../../../../src/agent/triage-provider.ts) supplies gateway-backed classification for scanner findings. Scanner rules live in [warlock](https://github.com/PostHog/warlock); Wizard owns @@ -119,16 +119,16 @@ terminates the run. Check each adapter's state machine when changing rejection handling. Preserve useful rejection diagnostics without logging secret content. Both SDK paths use a scoped gateway token minted through -[gateway-session.ts](../../../../src/lib/gateway-session.ts), not the user's raw +[gateway-session.ts](../../../../src/agent/gateway-session.ts), not the user's raw OAuth credential as a model API key. Rejection and refresh behavior belong at that seam; do not restore a legacy-gateway fallback to bypass admission. ### Secret vault: keeping values out of the model -[secret-vault.ts](../../../../src/lib/secret-vault.ts) stores user-provided +[secret-vault.ts](../../../../src/shared/secret-vault.ts) stores user-provided values in memory and returns opaque references. Sensitive `wizard_ask` answers become `secret:` refs; `set_env_values` resolves the ref host-side when -writing. Follow [wizard-tools](../../../../src/lib/wizard-tools/) and the Pi +writing. Follow [wizard-tools](../../../../src/agent/tools/) and the Pi adapters when adding a secret-consuming tool. Return references and metadata to the agent, never the raw value. References are session-scoped, not durable credentials. @@ -145,7 +145,7 @@ business logic. Harness adapters translate SDK messages, status markers, task updates and tool activity into WizardUI calls. Anthropic message processing lives in -[agent-interface.ts](../../../../src/lib/agent/agent-interface.ts); Pi uses its +[agent-interface.ts](../../../../src/agent/agent-interface.ts); Pi uses its own session event handlers. Orchestrated tasks also have queue and handoff state. Do not assume all harness output passes through `handleSDKMessage`. @@ -165,7 +165,7 @@ separate surfaces. The Anthropic SDK's MCP integration and Pi's adapter expose them differently; inspect the selected harness rather than assuming identical tool names or discovery. Context-mill supplies skills and flow/task prompts. -[Middleware](../../../../src/lib/middleware/) provides opt-in message/phase +[Middleware](../../../../src/agent/middleware/) provides opt-in message/phase instrumentation. The linear sequence creates the benchmark pipeline; there is no pipeline construction in the compatibility `agent-runner.ts`. Inspect the actual consumer before extending instrumentation to another sequence or harness. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 164f68233..48f68a2a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -20,4 +20,4 @@ /src/lib/programs/web-analytics-doctor/ @PostHog/team-web-analytics # Agent runner (harness, model, and tools) -/src/lib/agent/ @PostHog/team-wizard-docs +/src/agent/ @PostHog/team-wizard-docs diff --git a/README.md b/README.md index c66cefffb..8fbba68e9 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ route review to their owning team instead. | Path | Owning team | |---|---| | `*` (everything else, including all other programs) | `@PostHog/team-wizard-docs` | -| `src/lib/agent/` | `@PostHog/team-wizard-docs` | +| `src/agent/` | `@PostHog/team-wizard-docs` | | `src/lib/programs/posthog-integration/` | `@PostHog/team-wizard-docs` | | `src/lib/programs/error-tracking-upload-source-maps/` | `@PostHog/team-error-tracking` | | `src/lib/programs/mcp-analytics/` | `@PostHog/team-mcp-analytics` | @@ -270,7 +270,7 @@ dashboard:write insight:write notebook:write event_definition:write health_issue:read wizard_session:read wizard_session:write ``` -The source of truth is `WIZARD_OAUTH_SCOPES` in `src/lib/constants.ts`, which +The source of truth is `WIZARD_OAUTH_SCOPES` in `src/shared/constants.ts`, which documents why each scope is needed — if this block drifts, trust the code. Some programs request more on top (`PROGRAM_SCOPE_ADDITIONS` in `src/lib/oauth/program-scopes.ts`); the default integration flow adds @@ -291,7 +291,7 @@ are clamped silently (`clamp_scopes_to_ceiling`) — neither path errors; `/oauth/token` just returns a smaller `scope`. So never assume the token carries what was requested: the token response's `scope` field is the truth. The wizard diffs granted vs requested at login (`missingOAuthScopes` in -`src/utils/oauth.ts`), warns the user which permissions are missing, and emits +`src/shared/utils/oauth.ts`), warns the user which permissions are missing, and emits `wizard: oauth grant narrowed` so narrowed runs are countable in analytics. The diff also rides on the session (`credentials.missingScopes`), so when a run does fail on a scope-gated step, the error names the missing permission @@ -396,7 +396,7 @@ and set up the general flow of the application. ## Analytics Did you know you can capture PostHog events even for smaller, supporting -products like a command line tool? `src/utils/analytics.ts` is a great example +products like a command line tool? `src/shared/utils/analytics.ts` is a great example of how to do it. This file wraps `posthog-node` with some convenience functions to set up an @@ -417,7 +417,7 @@ set `POSTHOG_WIZARD_NO_TELEMETRY=1`) to disable. Supporting agent sessions after we leave is important. There are plenty of ways to break or misconfigure PostHog, so guarding against this is key. -`src/utils/rules/add-editor-rules.ts` demonstrates how to dynamically construct +`src/shared/utils/rules/add-editor-rules.ts` demonstrates how to dynamically construct rules files and store them in the project's `.cursor/rules` directory. ## Prompts and LLM interactions @@ -433,7 +433,7 @@ _If_ they are well prompted. chaotic process. Every wizard session gets the same prompt, tailored to the specific files in the project. -These prompts are channeled using `src/utils/query.ts` to an LLM interface we +These prompts are channeled using `src/shared/utils/query.ts` to an LLM interface we host. This gives us more control: we can be certain of the model version and provider which interpret the prompts and modify the files. This way, we can find the right tools for the job and again, apply them consistently. @@ -450,14 +450,14 @@ orchestrates that journey, but the raw value should _never_ enter the LLM conversation, where it would be sent to the model provider, written to transcripts, and captured in logs. -`src/lib/secret-vault.ts` is a small, reusable pattern for exactly this. It's a +`src/shared/secret-vault.ts` is a small, reusable pattern for exactly this. It's a session-scoped, in-memory vault: a tool that handles a secret calls `put()` to store the raw value and hands the agent an opaque `secret:` reference instead. The agent passes that ref between tools as if it were the value; the host resolves it back to the real secret only at the last moment, inside the process, when it writes the file. -Two tools in `src/lib/wizard-tools.ts` form the ends of that pipe: +Two tools in `src/agent/tools/tools.ts` form the ends of that pipe: - `wizard_ask` with `sensitive: true` vaults the user's typed answer and returns `{ secretRef: "secret:..." }` to the agent rather than the string. @@ -504,7 +504,9 @@ Path aliases defined in `tsconfig.build.json`, resolved by tsdown: |---|---| | `@env` | `src/env.ts` | | `@lib/*` | `src/lib/*` | -| `@utils/*` | `src/utils/*` | +| `@agent/*` | `src/agent/*` | +| `@shared/*` | `src/shared/*` | +| `@utils/*` | `src/shared/utils/*` | | `@ui/*` | `src/ui/*` | | `@steps/*` | `src/steps/*` | | `@frameworks/*` | `src/frameworks/*` | @@ -605,7 +607,7 @@ To make your version of a tool usable with a one-line `npx` command: # Health checks -`src/lib/health-checks/` checks skills download origins before the wizard runs. +`src/shared/health-checks/` checks skills download origins before the wizard runs. The entry point is `evaluateWizardReadiness()`, which only blocks on skill downloads: | Decision | Meaning | @@ -645,7 +647,7 @@ gateway URL and reports an unavailable gateway through the existing error path. `skillsOrigin` is one entry covering two origins: skills are published to GitHub Releases and an AWS mirror under the same filenames, and downloads fail -over between them (`src/lib/fetch-retry.ts`). Both are probed in parallel, so +over between them (`src/shared/fetch-retry.ts`). Both are probed in parallel, so the key only reports **Down** when neither origin answers — a GitHub Releases outage on its own doesn't block a run, including a 403 or 404, which is as often about the origin (expired asset redirect, blocked region, a publish that diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 87d7f9225..e5fadf4f7 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -102,7 +102,7 @@ Everything below ships in this repo (`wizard/`) and its workbench orchestrator on pi, per-task models from context-mill frontmatter; off → the linear anthropic default). Per-stage variations ride `wizard-orchestrator-override` payloads (`{stage: {model?, effort?}}`, - variant keys in `wizard/src/lib/agent/runner/switchboard/flags/schemes.ts`). + variant keys in `wizard/src/agent/runner/switchboard/flags/schemes.ts`). The baseline is `{"wizard-orchestrator":"false"}` — never an empty override, or live remote flags leak into the baseline. diff --git a/docs/error-catalog.md b/docs/error-catalog.md index dc1d90bdb..8e8c05b19 100644 --- a/docs/error-catalog.md +++ b/docs/error-catalog.md @@ -6,11 +6,11 @@ machine-readable code so the backend, the web UI, and sandbox supervisors can classify failures without parsing human-readable messages. - **Source of truth for codes:** - [`src/lib/errors/codes.ts`](../src/lib/errors/codes.ts) + [`src/shared/errors/codes.ts`](../src/shared/errors/codes.ts) - **Source of truth for metadata (group, retry advice, description):** - [`src/lib/errors/catalog.ts`](../src/lib/errors/catalog.ts) + [`src/shared/errors/catalog.ts`](../src/shared/errors/catalog.ts) - **Consumers:** `wizardAbort()` - ([`src/utils/wizard-abort.ts`](../src/utils/wizard-abort.ts)), the task stream + ([`src/shared/utils/wizard-abort.ts`](../src/shared/utils/wizard-abort.ts)), the task stream ([`src/lib/task-stream/`](../src/lib/task-stream/)), and non-interactive hosts reading stderr. @@ -117,7 +117,7 @@ guarantee. Program detect steps write `{ kind, ...detail }` into `session.frameworkContext.detectError`. `detectErrorCode()` -([`src/lib/errors/detect-map.ts`](../src/lib/errors/detect-map.ts)) maps `kind` +([`src/shared/errors/detect-map.ts`](../src/shared/errors/detect-map.ts)) maps `kind` → code, and the whole object — `kind` included — rides along as `OutroData.errorDetail`. @@ -140,7 +140,7 @@ Two rules make the detect group safe for automated retry policy: ## Auth classification Gateway 401s are classified at the abort site by `classifyAuthFailure()` -([`src/lib/errors/auth.ts`](../src/lib/errors/auth.ts)) with priority: +([`src/shared/errors/auth.ts`](../src/shared/errors/auth.ts)) with priority: stored-login conflict → settings conflict → key-type → missing scope → region mismatch → invalid/expired. Inputs are best-effort from the run context; the classifier degrades to `PHW_AUTH_INVALID_OR_EXPIRED` when no distinguishing diff --git a/docs/local-dev.md b/docs/local-dev.md index 10e11ba7b..93f949cdd 100644 --- a/docs/local-dev.md +++ b/docs/local-dev.md @@ -170,7 +170,7 @@ skills url, MCP url, and PostHog host together. ## Implementation -`src/lib/local-dev.ts` defines the endpoints and precedence. Downstream code +`src/shared/local-dev.ts` defines the endpoints and precedence. Downstream code reads the resolved targets. The three service flags have no yargs default. An absent flag is `undefined` diff --git a/docs/runbooks/aio-capture.md b/docs/runbooks/aio-capture.md index 667208d3c..a74f29c9f 100644 --- a/docs/runbooks/aio-capture.md +++ b/docs/runbooks/aio-capture.md @@ -76,7 +76,7 @@ dev-only concern. - **`agents-platform` harness** — placeholder today (`README` only). When it lands, whoever builds it needs to add a third transform + wire point in - `src/lib/agent/aio-capture.ts`. Search for `captureFromPiMessageEndEvent` + `src/agent/aio-capture.ts`. Search for `captureFromPiMessageEndEvent` for the pattern. - **Non-assistant messages** — user turns, tool results, and system messages don't get their own `$ai_generation`. Tool calls ride as content blocks diff --git a/docs/runbooks/warlock-kill-switch.md b/docs/runbooks/warlock-kill-switch.md index 7b0189767..33979e4d5 100644 --- a/docs/runbooks/warlock-kill-switch.md +++ b/docs/runbooks/warlock-kill-switch.md @@ -63,7 +63,7 @@ Narrower is better — keep the security net on for users who aren't affected. ## Timing & reach - The flag is read **once at run start** - ([`agent-runner.ts`](../../src/lib/agent/agent-runner.ts), + ([`agent-runner.ts`](../../src/agent/agent-runner.ts), `getAllFlagsForWizard()`), so a change takes effect on the **next** run — not on runs already in flight. Wizard runs are short-lived, so "next run" is effectively seconds to minutes. @@ -90,10 +90,10 @@ is a **local tool only** — it is per-machine and is **not** the incident lever One feature flag, read at run start, gates the two hooks Warlock registers. - **Flag key:** `wizard-warlock-disabled` — - [`src/lib/constants.ts`](../../src/lib/constants.ts) + [`src/shared/constants.ts`](../../src/shared/constants.ts) (`WIZARD_WARLOCK_DISABLED_FLAG_KEY`). - **Decision helper:** `isWarlockDisabled(flags)` in - [`src/lib/agent/agent-interface.ts`](../../src/lib/agent/agent-interface.ts) — + [`src/agent/agent-interface.ts`](../../src/agent/agent-interface.ts) — pure, unit-tested. - **The gate:** at hook registration in the same file, when disabled the Pre/PostToolUse YARA hooks are registered as empty arrays (`[]`) instead of @@ -114,7 +114,7 @@ exact string `'true'`. Everything else leaves Warlock **ON**: A network blip must never silently disable a security control. This is pinned by unit tests in -[`src/lib/__tests__/agent-interface.test.ts`](../../src/lib/__tests__/agent-interface.test.ts) +[`src/agent/__tests__/agent-interface.test.ts`](../../src/agent/__tests__/agent-interface.test.ts) (`describe('isWarlockDisabled (kill switch)')`). --- @@ -146,5 +146,5 @@ unit tests in - Scanner engine & rules: the [warlock](https://github.com/PostHog/warlock) sibling repo. -- Hook wiring: [`src/lib/yara-hooks.ts`](../../src/lib/yara-hooks.ts). +- Hook wiring: [`src/agent/yara-hooks.ts`](../../src/agent/yara-hooks.ts). - Feedback / questions: wizard@posthog.com. diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 241bcab22..f5e28a1cc 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -24,7 +24,7 @@ node --input-type=module -e "import '$DIST_BIN'" 2>&1 | head -5 | grep -q 'PostH } # ── 2. CI flag overrides physically absent from production builds ─────────── -# The override path (src/utils/ci-flag-overrides.ts) is dead code in published +# The override path (src/shared/utils/ci-flag-overrides.ts) is dead code in published # builds and tsdown strips it; its env var name appearing in dist/*.js means # dead-code elimination regressed and a prod surface leaked. Sourcemaps keep # the original source, so only .js output counts. diff --git a/src/lib/programs/__tests__/mcp-analytics.test.ts b/src/lib/programs/__tests__/mcp-analytics.test.ts index e9a8546dc..a2c822c00 100644 --- a/src/lib/programs/__tests__/mcp-analytics.test.ts +++ b/src/lib/programs/__tests__/mcp-analytics.test.ts @@ -7,7 +7,7 @@ describe('MCP_ANALYTICS_ABORT_CASES', () => { // These are the exact `[ABORT] ` strings the mcp-analytics skill // emits (context-mill `context/skills/mcp-analytics/description.md`), with // the `[ABORT] ` prefix already stripped — matching what the runner passes - // to `AbortCase.match` (src/lib/agent/runner/sequence/linear.ts). + // to `AbortCase.match` (src/agent/runner/sequence/linear.ts). const reasons = [ 'no mcp server found', 'unsupported language for mcp analytics', diff --git a/src/lib/programs/self-driving/ARCHITECTURE.md b/src/lib/programs/self-driving/ARCHITECTURE.md index 96408a2d0..39f4f8cbf 100644 --- a/src/lib/programs/self-driving/ARCHITECTURE.md +++ b/src/lib/programs/self-driving/ARCHITECTURE.md @@ -25,7 +25,7 @@ anchors are point-in-time — the symbol names are the durable part. | The ordered steps | `src/lib/programs/self-driving/prompt.ts` | | What each step _does_ | `context-mill/context/skills/self-driving/references/*.md` | | Program registration / lifecycle | `src/lib/programs/self-driving/index.ts` | -| `wizard_ask` / `.env` tools | `src/lib/wizard-tools.ts`, `src/lib/wizard-ask-bridge.ts` | +| `wizard_ask` / `.env` tools | `src/agent/tools/tools.ts`, `src/agent/wizard-ask-bridge.ts` | | OAuth scopes (+ prod ceiling) | `src/lib/oauth/program-scopes.ts` (§3, §7) | | Signals models / MCP / sync | `posthog/products/signals/backend/…` (§5) | | Why a team gets no findings | §6 | @@ -167,10 +167,10 @@ in `buildOutroData` (no auth deep-link — §7 item 7). CLI: (the agent's working dir and detection target). **Runner & agent loop (generic — not Signals-aware).** `runProgram` -(`src/lib/agent/agent-runner.ts`) is the fixed pipeline +(`src/agent/agent-runner.ts`) is the fixed pipeline `init → health → settings → OAuth → skill install → agent → run → errors → postRun → outro`. It installs the skill by ID, resolves the MCP URL, runs the Claude Agent SDK -`query()` (`src/lib/agent/agent-interface.ts`) in a sandbox with the +`query()` (`src/agent/agent-interface.ts`) in a sandbox with the `posthog-wizard` + `wizard-tools` MCP servers, and parses agent output: `[STATUS]` → UI, `[ABORT] ` → terminal `AgentErrorType.ABORT` matched against `config.abortCases`. `PromptContext` (project/host + AI-consent @@ -179,7 +179,7 @@ against `config.abortCases`. `PromptContext` (project/host + AI-consent `buildSelfDrivingPrompt`. Anything deeper here is generic machinery — read those two files directly. -**`wizard-tools` MCP + `wizard_ask`** (`src/lib/wizard-tools.ts`). +**`wizard-tools` MCP + `wizard_ask`** (`src/agent/tools/tools.ts`). `check_env_keys` / `set_env_values` are the only sanctioned `.env` access (value-safe, `.gitignore`-guarded, secret-vault aware). `wizard_ask` is the **only** way to ask the user anything — 1–8 questions, capped at `maxQuestions` @@ -196,12 +196,12 @@ A multi-select's default focus is its first enabled option and an empty `enter` submits that focused option — which is why a **decline option, when present, is placed first** (it becomes the safe default). No bridge (CI/non-interactive) → returns an error telling the agent to default or emit -`[ABORT] requires-interactive-mode`. The bridge (`src/lib/wizard-ask-bridge.ts`) +`[ABORT] requires-interactive-mode`. The bridge (`src/agent/wizard-ask-bridge.ts`) brokers into the TUI overlay; cancelled/timed-out fields resolve to `CANCELLED_SENTINEL = '__cancelled__'`. **OAuth scopes** (`src/lib/oauth/program-scopes.ts`). Base `WIZARD_OAUTH_SCOPES` -(`src/lib/constants.ts`) ∪ `SELF_DRIVING_SCOPE_ADDITIONS` — **12 strings**, +(`src/shared/constants.ts`) ∪ `SELF_DRIVING_SCOPE_ADDITIONS` — **12 strings**, requested via a PKCE auth-code flow: | Scope | Why | @@ -226,7 +226,7 @@ requests none. (An *exhaustive* ceiling — no `@default` — is possible and wo reject anything unlisted, but the wizard apps aren't configured that way.) See §7 item 1 and the README's "OAuth app scope ceiling". -**Security & TUI.** YARA hooks (`src/lib/yara-hooks.ts`) scan +**Security & TUI.** YARA hooks (`src/agent/yara-hooks.ts`) scan Bash/Write/Edit/Read content and installed skills via the `warlock` scanner (fail-closed; categories: prompt injection, exfiltration, destructive ops, supply-chain, secrets, PII); a critical match aborts the run. New rules go in @@ -260,7 +260,7 @@ The canonical `signals-scout-*` skills do **not** live here — they're in posth hot-rebuilds individual skill zips but **not** the bundle. Release: a PR to `main` with the **`mcp-publish`** label builds and force-moves the `latest` GitHub release tag. The wizard resolves the skill ID at runtime against - `getSkillsBaseUrl(localMcp)` (`src/lib/constants.ts`): + `getSkillsBaseUrl(localMcp)` (`src/shared/constants.ts`): `…/releases/latest/download` (prod) or `localhost:8765` (`--local-mcp`) — so skill content is decoupled from the wizard npm release (and a prod wizard is broken until the skill is published to `latest`; §7). @@ -358,7 +358,7 @@ source is enabled. > and the `[ABORT] self-driving is not available for this project` path is now > only a safety net for a genuine Signals-API outage. -1. **UI flag `product-autonomy`** (`posthog/frontend/src/lib/constants.tsx`, +1. **UI flag `product-autonomy`** (`posthog/frontend/src/shared/constants.tsx`, `FEATURE_FLAGS.PRODUCT_AUTONOMY`). Frontend-only — gates the Inbox scene, nav item, and source-config loading. Off → the user can't _see_ the inbox; the pipeline is unaffected. diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index af7fdd77e..18edf4972 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -140,7 +140,7 @@ export interface WizardSession { * `--capture-aio`: mirror every wizard LLM call as an `$ai_generation` event * into the authenticated project's AI Observability tab. Dev/test builds * only — the flag is undeclared in published builds so this stays `false` - * there. See `src/lib/agent/aio-capture.ts`. + * there. See `src/agent/aio-capture.ts`. */ captureAio: boolean; @@ -204,7 +204,7 @@ export interface WizardSession { /** * Full user payload from `/api/users/@me/` — identifiers, profile, * current team + organization, preferences, etc. Null until OAuth / - * CI-key auth populates it. Schema lives in `src/lib/api.ts` and + * CI-key auth populates it. Schema lives in `src/shared/api.ts` and * passes through unknown upstream fields so downstream features can * read account context (plan, org name, email, etc.) without * re-fetching. diff --git a/src/shared/constants.ts b/src/shared/constants.ts index c055778ce..0cbaf37d0 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -39,8 +39,8 @@ export const GPT5_6_SOL_MODEL = 'openai/gpt-5.6-sol'; * — `Object.values(Harness)` gives an iterable of the values when an array is * needed. Adding a member is enough to pick it up everywhere. * - * Naming matches the directory layout — see `src/lib/agent/runner/harness/` - * and `src/lib/agent/runner/sequence/`. + * Naming matches the directory layout — see `src/agent/runner/harness/` + * and `src/agent/runner/sequence/`. */ export enum Harness { anthropic = 'anthropic', diff --git a/src/shared/health-checks/testme.md b/src/shared/health-checks/testme.md index ba5b04187..31df3aca7 100644 --- a/src/shared/health-checks/testme.md +++ b/src/shared/health-checks/testme.md @@ -3,7 +3,7 @@ Run the existing health and gateway tests with mocked HTTP requests: ```bash -pnpm exec vitest run src/lib/health-checks/__tests__/health-checks.test.ts src/lib/__tests__/gateway-session.test.ts +pnpm exec vitest run src/shared/health-checks/__tests__/health-checks.test.ts src/agent/__tests__/gateway-session.test.ts ``` The checks cover gateway readiness, endpoint retries, and skills downloads from From 580c5069b42606125463eb206cf70ce607af5956 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 13:45:40 -0400 Subject: [PATCH 25/61] refactor(shared): move the skill menu out of the agent Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../architecture/known-violations.json | 1 - src/__tests__/programs-cli.test.ts | 6 +- src/__tests__/skill-cli.test.ts | 6 +- .../__tests__/run-agent-standalone.test.ts | 4 +- src/agent/__tests__/wizard-tools.test.ts | 45 ---------- src/agent/runner/harness/pi/tools.ts | 2 +- .../__tests__/variant-resolution.test.ts | 3 +- .../orchestrator/orchestrator-runner.ts | 7 +- src/agent/tools/mcp.ts | 3 +- src/agent/tools/tools.ts | 90 +------------------ .../factories/family-command-factory.ts | 2 +- src/commands/skill.ts | 2 +- src/lib/__tests__/wizard-spellbook.test.ts | 8 +- src/lib/programs/dispatch-family.ts | 2 +- src/lib/wizard-spellbook.ts | 4 +- src/shared/__tests__/skill-menu.test.ts | 90 +++++++++++++++++++ src/shared/skill-menu.ts | 90 +++++++++++++++++++ src/ui/tui/__tests__/frames.test.tsx | 5 +- .../__tests__/keyboard-equivalence.test.tsx | 4 +- src/ui/tui/__tests__/skill-entry.test.ts | 2 +- src/ui/tui/screens/SkillSourceInfo.tsx | 2 +- .../tui/screens/health/HealthCheckScreen.tsx | 3 +- 22 files changed, 215 insertions(+), 166 deletions(-) create mode 100644 src/shared/__tests__/skill-menu.test.ts create mode 100644 src/shared/skill-menu.ts diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 1f0f7bd21..0eecf3f85 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -66,7 +66,6 @@ "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", "src/ui/agent-progress.ts -> src/agent/progress.ts", "src/ui/index.ts -> src/ui/logging-ui.ts", - "src/ui/tui/screens/SkillSourceInfo.tsx -> src/agent/tools/index.ts", "src/ui/tui/screens/health/HealthCheckScreen.tsx -> src/agent/tools/index.ts", "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/mcp-prompt-streaming.ts", "src/ui/wizard-ui.ts -> src/agent/progress.ts" diff --git a/src/__tests__/programs-cli.test.ts b/src/__tests__/programs-cli.test.ts index 9f24ddd10..c7a5e074e 100644 --- a/src/__tests__/programs-cli.test.ts +++ b/src/__tests__/programs-cli.test.ts @@ -8,8 +8,8 @@ vi.mock('@lib/runners', () => ({ runWizardCI: mockRunWizardCI, })); -vi.mock('@agent/tools', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@shared/skill-menu', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, fetchSkillMenu: vi.fn(), @@ -31,7 +31,7 @@ import { pickerChildrenToShow, } from '@lib/programs/dispatch-family'; import type { Command } from '../commands/command'; -import { fetchSkillMenu, type CliEntry } from '@agent/tools'; +import { fetchSkillMenu, type CliEntry } from '@shared/skill-menu'; import { auditConfig } from '@lib/programs/audit/index'; import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; import { parseCommand } from './helpers/parse-command.no-jest'; diff --git a/src/__tests__/skill-cli.test.ts b/src/__tests__/skill-cli.test.ts index aed48a047..676023cf1 100644 --- a/src/__tests__/skill-cli.test.ts +++ b/src/__tests__/skill-cli.test.ts @@ -4,13 +4,13 @@ vi.mock('../commands/basic-integration/skill', () => ({ runSkillMode: vi.fn(), })); -vi.mock('@agent/tools', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@shared/skill-menu', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, fetchSkillMenu: vi.fn() }; }); import { runSkillMode } from '../commands/basic-integration/skill'; -import { fetchSkillMenu } from '@agent/tools'; +import { fetchSkillMenu } from '@shared/skill-menu'; import { analytics } from '@utils/analytics'; import { skillCommand } from '../commands/skill'; import { parseCommand } from './helpers/parse-command.no-jest'; diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index 9e436dbe4..d03bd9890 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -174,8 +174,8 @@ vi.mock('@agent/agent-prompt-loader', async (original) => { ), }; }); -vi.mock('@agent/tools', async (original) => ({ - ...(await original()), +vi.mock('@shared/skill-menu', async (original) => ({ + ...(await original()), fetchSkillMenu: vi.fn().mockResolvedValue({ categories: {} }), })); diff --git a/src/agent/__tests__/wizard-tools.test.ts b/src/agent/__tests__/wizard-tools.test.ts index 40bbd129e..d61a953b1 100644 --- a/src/agent/__tests__/wizard-tools.test.ts +++ b/src/agent/__tests__/wizard-tools.test.ts @@ -21,7 +21,6 @@ import { ensureGitignoreCoverage, describeAskCancellation, evaluateAskCap, - fetchSkillMenu, mergeEnvValues, normaliseAskSubject, parseEnvKeys, @@ -1398,47 +1397,3 @@ describe('downloadSkill (e2e over HTTP)', () => { } }); }); - -describe('fetchSkillMenu', () => { - const noSleep = () => Promise.resolve(); - const menu = { categories: { integration: [] } }; - const menuResponse = () => - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', - json: () => Promise.resolve(menu), - }); - - it('retries a flaky menu fetch before succeeding', async () => { - let attempts = 0; - - const result = await fetchSkillMenu('http://localhost:8765', { - fetchImpl: (() => { - attempts += 1; - if (attempts < 3) return Promise.reject(new Error('reset')); - return menuResponse(); - }) as any, - sleepImpl: noSleep, - }); - - expect(attempts).toBe(3); - expect(result).toEqual(menu); - }); - - it('returns null after exhausting retries', async () => { - let attempts = 0; - - const result = await fetchSkillMenu('http://localhost:8765', { - fetchImpl: (() => { - attempts += 1; - return Promise.reject(new Error('network down')); - }) as any, - sleepImpl: noSleep, - maxAttempts: 3, - }); - - expect(attempts).toBe(3); - expect(result).toBeNull(); - }); -}); diff --git a/src/agent/runner/harness/pi/tools.ts b/src/agent/runner/harness/pi/tools.ts index 06c752160..a24daacc5 100644 --- a/src/agent/runner/harness/pi/tools.ts +++ b/src/agent/runner/harness/pi/tools.ts @@ -38,7 +38,6 @@ import { createAskAccounting, describeAskCancellation, ensureGitignoreCoverage, - fetchSkillMenu, installSkillById, mergeEnvValues, normaliseAskSubject, @@ -53,6 +52,7 @@ import { WIZARD_ASK_SUBJECT_DESCRIPTION, WIZARD_ASK_TOOL_DESCRIPTION, } from '@agent/tools/tools'; +import { fetchSkillMenu } from '@shared/skill-menu'; import type { LLMProvider } from '@posthog/warlock'; import type { ProgressEmitter } from '@agent/progress'; import { diff --git a/src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts index afbc48a15..155c51e9c 100644 --- a/src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/variant-resolution.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect } from 'vitest'; import { resolveSkillVariantId } from '../orchestrator-runner'; import { Integration } from '@shared/constants'; -import { expandBundleEntry } from '@agent/tools'; -import type { SkillEntry } from '@agent/tools'; +import { expandBundleEntry, type SkillEntry } from '@shared/skill-menu'; // Pinned from the real built skill-menu.json, so this suite tests the actual cross-repo contract. const INTEGRATION_ENTRIES = [ diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 7233f4b8f..2d28b1d61 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -24,11 +24,8 @@ import { import * as path from 'path'; import { OutroKind, type TaskNotice } from '@agent/progress'; import { POSTHOG_DOCS_URL, WIZARD_CONTACT_EMAIL } from '@shared/constants'; -import { - installSkillById, - fetchSkillMenu, - type SkillEntry, -} from '@agent/tools'; +import { installSkillById } from '@agent/tools'; +import { fetchSkillMenu, type SkillEntry } from '@shared/skill-menu'; import { analytics } from '@utils/analytics'; import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; diff --git a/src/agent/tools/mcp.ts b/src/agent/tools/mcp.ts index 84c4b79a6..ce01bc29e 100644 --- a/src/agent/tools/mcp.ts +++ b/src/agent/tools/mcp.ts @@ -45,7 +45,6 @@ import { ensureGitignoreCoverage, createAskAccounting, describeAskCancellation, - fetchSkillMenu, checkEnvKeys as checkEnvKeysCore, mergeEnvValues, normaliseAskSubject, @@ -57,7 +56,6 @@ import { templateEnvWriteRefusal, legacyKeyNameRefusal, vaultSensitiveAnswers, - type SkillEntry, AUDIT_ADD_CHECKS_DESCRIPTION, AUDIT_ADD_CHECKS_PARAM_DESCRIPTION, AUDIT_RESOLVE_CHECKS_DESCRIPTION, @@ -70,6 +68,7 @@ import { WIZARD_ASK_SUBJECT_DESCRIPTION, WIZARD_ASK_TOOL_DESCRIPTION, } from './tools'; +import { fetchSkillMenu, type SkillEntry } from '@shared/skill-menu'; const auditCheckSchema = z.object({ id: z.string().min(1), diff --git a/src/agent/tools/tools.ts b/src/agent/tools/tools.ts index c33ff5e80..7c1421017 100644 --- a/src/agent/tools/tools.ts +++ b/src/agent/tools/tools.ts @@ -32,26 +32,7 @@ import { import { CANCELLED_SENTINEL } from '../wizard-ask-bridge'; import type { SecretVault } from '@shared/secret-vault'; import { fetchWithRetry, type RetryOpts } from '@shared/fetch-retry'; - -// --------------------------------------------------------------------------- -// Skill types -// --------------------------------------------------------------------------- - -export type SkillEntry = { - id: string; - name: string; - downloadUrl: string; - /** The hyphenated skill-group prefix of `id` (e.g. `posthog-integration-install`). */ - group?: string; - /** The detection id this variant serves (e.g. `rails`, `react-router`). */ - framework?: string; - /** The variant a bare framework id resolves to when its family has several. */ - default?: boolean; - /** This entry's download is a bundle JSON of every variant, not a single skill's zip. */ - bundle?: boolean; - /** Menu-only: the variants inside a bundle, expanded into entries of their own on fetch. */ - variants?: { id: string; framework?: string; default?: boolean }[]; -}; +import { fetchSkillMenu, type SkillEntry } from '@shared/skill-menu'; /** A bundle's files, keyed by variant short id then path. */ export type SkillBundle = { @@ -59,75 +40,6 @@ export type SkillBundle = { variants: Record>; }; -/** - * Entry in the wizard's runtime CLI registry. Mirrors the shape context-mill - * publishes under `cliEntries` inside `skill-menu.json`. The wizard uses these - * to register skill-backed subcommands at runtime instead of from a baked - * build-time snapshot. - */ -export type CliEntry = { - skillId: string; - role: 'command' | 'skill' | 'internal'; - command?: string; - parentCommand?: string; - default?: boolean; - displayName: string; - description: string; -}; - -export interface SkillMenu { - categories: Record; - /** - * Skills exposed as CLI commands. Optional because context-mill releases - * older than the runtime-resolver cutover don't emit this field. - */ - cliEntries?: CliEntry[]; -} - -// --------------------------------------------------------------------------- -// Standalone skill helpers (usable before the MCP server is created) -// --------------------------------------------------------------------------- - -/** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */ -export function expandBundleEntry(entry: SkillEntry): SkillEntry[] { - if (!entry.bundle || !entry.variants) return [entry]; - return entry.variants.map((variant) => ({ - ...variant, - name: entry.name, - group: entry.group, - bundle: true, - downloadUrl: entry.downloadUrl, - })); -} - -/** - * Fetch the skill menu from the skills server. - * Returns parsed data on success, `null` on failure. - */ -export async function fetchSkillMenu( - skillsBaseUrl: string, - opts: RetryOpts = {}, -): Promise { - const menuUrl = `${skillsBaseUrl}/skill-menu.json`; - try { - logToFile(`fetchSkillMenu: fetching from ${menuUrl}`); - const resp = await fetchWithRetry(menuUrl, opts); - const data = (await resp.json()) as SkillMenu; - for (const [category, entries] of Object.entries(data.categories)) { - data.categories[category] = entries.flatMap(expandBundleEntry); - } - logToFile( - `fetchSkillMenu: loaded (${ - Object.keys(data.categories).length - } categories)`, - ); - return data; - } catch (err: any) { - logToFile(`fetchSkillMenu: error: ${err.message}`); - return null; - } -} - /** Extract a zip buffer, refusing entries that escape destDir (zip-slip). */ function extractZipArchive(zip: Uint8Array, destDir: string): number { const root = path.resolve(destDir); diff --git a/src/commands/factories/family-command-factory.ts b/src/commands/factories/family-command-factory.ts index 9ae0f164c..087bd5d21 100644 --- a/src/commands/factories/family-command-factory.ts +++ b/src/commands/factories/family-command-factory.ts @@ -7,7 +7,7 @@ import { pickerChildrenToShow, } from '@lib/programs/dispatch-family'; import { getSkillsBaseUrl } from '@shared/constants'; -import { fetchSkillMenu } from '@agent/tools'; +import { fetchSkillMenu } from '@shared/skill-menu'; import type { Command } from '../command'; import { createFamilyPickerDefault } from './family-picker'; diff --git a/src/commands/skill.ts b/src/commands/skill.ts index c72fb93ec..0ffb51adf 100644 --- a/src/commands/skill.ts +++ b/src/commands/skill.ts @@ -1,7 +1,7 @@ import type { Arguments } from 'yargs'; import { getSkillsBaseUrl } from '@shared/constants'; -import { fetchSkillMenu, type CliEntry } from '@agent/tools'; +import { fetchSkillMenu, type CliEntry } from '@shared/skill-menu'; import { analytics } from '@utils/analytics'; import { runSkillMode } from './basic-integration/skill'; diff --git a/src/lib/__tests__/wizard-spellbook.test.ts b/src/lib/__tests__/wizard-spellbook.test.ts index e23af172b..2f15a66b7 100644 --- a/src/lib/__tests__/wizard-spellbook.test.ts +++ b/src/lib/__tests__/wizard-spellbook.test.ts @@ -5,13 +5,17 @@ import { Integration } from '@shared/constants'; import type { ProgramConfig } from '../programs/program-step'; import { buildSession } from '../wizard-session'; import { writeWizardSpellbook } from '../wizard-spellbook'; -import { downloadSkill, fetchSkillMenu } from '@agent/tools/tools'; +import { downloadSkill } from '@agent/tools/tools'; +import { fetchSkillMenu } from '@shared/skill-menu'; vi.mock('@agent/tools/tools', async (importOriginal) => ({ ...(await importOriginal()), - fetchSkillMenu: vi.fn(), downloadSkill: vi.fn(), })); +vi.mock('@shared/skill-menu', async (importOriginal) => ({ + ...(await importOriginal()), + fetchSkillMenu: vi.fn(), +})); const program: ProgramConfig = { id: 'example-setup', diff --git a/src/lib/programs/dispatch-family.ts b/src/lib/programs/dispatch-family.ts index e36831674..f2922077b 100644 --- a/src/lib/programs/dispatch-family.ts +++ b/src/lib/programs/dispatch-family.ts @@ -7,7 +7,7 @@ import { agentSkillConfig } from '@lib/programs/program-registry'; import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; import type { ProgramConfig } from '@lib/programs/program-step'; import { getSkillsBaseUrl } from '@shared/constants'; -import { fetchSkillMenu, type CliEntry } from '@agent/tools'; +import { fetchSkillMenu, type CliEntry } from '@shared/skill-menu'; import { analytics } from '@utils/analytics'; import { dispatchProgram } from '../../commands/factories/shared'; diff --git a/src/lib/wizard-spellbook.ts b/src/lib/wizard-spellbook.ts index 894ae1f6e..be7105e51 100644 --- a/src/lib/wizard-spellbook.ts +++ b/src/lib/wizard-spellbook.ts @@ -3,12 +3,12 @@ import path from 'path'; import { getSkillsBaseUrl, POSTHOG_DOCS_URL } from '@shared/constants'; import type { ProgramConfig } from './programs/program-step'; import type { WizardSession } from './wizard-session'; +import { downloadSkill } from '@agent/tools/tools'; import { - downloadSkill, fetchSkillMenu, type SkillEntry, type SkillMenu, -} from '@agent/tools/tools'; +} from '@shared/skill-menu'; export type WizardSpellbook = { path: string; skillsIncluded: boolean }; type SpellbookSession = Pick< diff --git a/src/shared/__tests__/skill-menu.test.ts b/src/shared/__tests__/skill-menu.test.ts new file mode 100644 index 000000000..bc5884f0d --- /dev/null +++ b/src/shared/__tests__/skill-menu.test.ts @@ -0,0 +1,90 @@ +import { fetchSkillMenu, type SkillMenu } from '@shared/skill-menu'; + +describe('fetchSkillMenu', () => { + const noSleep = () => Promise.resolve(); + const menu: SkillMenu = { categories: { integration: [] } }; + const respond = (body: unknown) => () => + Promise.resolve({ + ok: true, + status: 200, + statusText: 'OK', + json: () => Promise.resolve(body), + }); + + it('retries a flaky menu fetch before succeeding', async () => { + let attempts = 0; + + const result = await fetchSkillMenu('http://localhost:8765', { + fetchImpl: (() => { + attempts += 1; + if (attempts < 3) return Promise.reject(new Error('reset')); + return respond(menu)(); + }) as any, + sleepImpl: noSleep, + }); + + expect(attempts).toBe(3); + expect(result).toEqual(menu); + }); + + it('returns null after exhausting retries', async () => { + let attempts = 0; + + const result = await fetchSkillMenu('http://localhost:8765', { + fetchImpl: (() => { + attempts += 1; + return Promise.reject(new Error('network down')); + }) as any, + sleepImpl: noSleep, + maxAttempts: 3, + }); + + expect(attempts).toBe(3); + expect(result).toBeNull(); + }); + + it('expands a bundle entry into one entry per variant', async () => { + const bundled: SkillMenu = { + categories: { + integration: [ + { + id: 'capture', + name: 'Capture', + group: 'capture', + bundle: true, + downloadUrl: 'http://localhost:8765/capture.json', + variants: [ + { id: 'capture-rails', framework: 'rails', default: true }, + { id: 'capture-react', framework: 'react' }, + ], + }, + ], + }, + }; + + const result = await fetchSkillMenu('http://localhost:8765', { + fetchImpl: respond(bundled) as any, + sleepImpl: noSleep, + }); + + expect(result?.categories.integration).toEqual([ + { + id: 'capture-rails', + framework: 'rails', + default: true, + name: 'Capture', + group: 'capture', + bundle: true, + downloadUrl: 'http://localhost:8765/capture.json', + }, + { + id: 'capture-react', + framework: 'react', + name: 'Capture', + group: 'capture', + bundle: true, + downloadUrl: 'http://localhost:8765/capture.json', + }, + ]); + }); +}); diff --git a/src/shared/skill-menu.ts b/src/shared/skill-menu.ts new file mode 100644 index 000000000..41430e38c --- /dev/null +++ b/src/shared/skill-menu.ts @@ -0,0 +1,90 @@ +/** + * The skill menu: what context-mill publishes as `skill-menu.json` and how the + * wizard reads it. Skill-backed CLI commands, the family picker, the skill + * source screen and the agent's install path all resolve skills through this + * one fetch, so it lives in shared and depends on nothing in the agent. + */ + +import { logToFile } from '@utils/debug'; +import { fetchWithRetry, type RetryOpts } from '@shared/fetch-retry'; + +export type SkillEntry = { + id: string; + name: string; + downloadUrl: string; + /** The hyphenated skill-group prefix of `id` (e.g. `posthog-integration-install`). */ + group?: string; + /** The detection id this variant serves (e.g. `rails`, `react-router`). */ + framework?: string; + /** The variant a bare framework id resolves to when its family has several. */ + default?: boolean; + /** This entry's download is a bundle JSON of every variant, not a single skill's zip. */ + bundle?: boolean; + /** Menu-only: the variants inside a bundle, expanded into entries of their own on fetch. */ + variants?: { id: string; framework?: string; default?: boolean }[]; +}; + +/** + * Entry in the wizard's runtime CLI registry. Mirrors the shape context-mill + * publishes under `cliEntries` inside `skill-menu.json`. The wizard uses these + * to register skill-backed subcommands at runtime instead of from a baked + * build-time snapshot. + */ +export type CliEntry = { + skillId: string; + role: 'command' | 'skill' | 'internal'; + command?: string; + parentCommand?: string; + default?: boolean; + displayName: string; + description: string; +}; + +export interface SkillMenu { + categories: Record; + /** + * Skills exposed as CLI commands. Optional because context-mill releases + * older than the runtime-resolver cutover don't emit this field. + */ + cliEntries?: CliEntry[]; +} + +/** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */ +export function expandBundleEntry(entry: SkillEntry): SkillEntry[] { + if (!entry.bundle || !entry.variants) return [entry]; + return entry.variants.map((variant) => ({ + ...variant, + name: entry.name, + group: entry.group, + bundle: true, + downloadUrl: entry.downloadUrl, + })); +} + +/** + * Fetch the skill menu from the skills server. + * Returns parsed data on success, `null` on failure. + */ +export async function fetchSkillMenu( + skillsBaseUrl: string, + opts: RetryOpts = {}, +): Promise { + const menuUrl = `${skillsBaseUrl}/skill-menu.json`; + try { + logToFile(`fetchSkillMenu: fetching from ${menuUrl}`); + const resp = await fetchWithRetry(menuUrl, opts); + const data = (await resp.json()) as SkillMenu; + for (const [category, entries] of Object.entries(data.categories)) { + data.categories[category] = entries.flatMap(expandBundleEntry); + } + logToFile( + `fetchSkillMenu: loaded (${ + Object.keys(data.categories).length + } categories)`, + ); + return data; + } catch (err: any) { + logToFile(`fetchSkillMenu: error: ${err.message}`); + return null; + } +} diff --git a/src/ui/tui/__tests__/frames.test.tsx b/src/ui/tui/__tests__/frames.test.tsx index c24a782af..49bfb6062 100644 --- a/src/ui/tui/__tests__/frames.test.tsx +++ b/src/ui/tui/__tests__/frames.test.tsx @@ -52,9 +52,12 @@ vi.mock('@shared/api', async (actual) => ({ })); vi.mock('@agent/tools', async (actual) => ({ ...(await actual>()), - fetchSkillMenu: vi.fn(pending), downloadSkill: vi.fn(pending), })); +vi.mock('@shared/skill-menu', async (actual) => ({ + ...(await actual>()), + fetchSkillMenu: vi.fn(pending), +})); vi.mock('@ui/tui/hooks/useGithubConnection', () => ({ useGithubConnection: () => undefined, fetchLoginUrl: vi.fn().mockResolvedValue(null), diff --git a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx index e240e36dd..1a5c27db2 100644 --- a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx +++ b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx @@ -66,8 +66,8 @@ vi.mock('@shared/api', async (importOriginal) => ({ fetchSlackConnected: vi.fn().mockResolvedValue(false), fetchUserData: vi.fn(() => new Promise(() => undefined)), })); -vi.mock('@agent/tools', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@shared/skill-menu', async (importOriginal) => ({ + ...(await importOriginal()), fetchSkillMenu: vi.fn(() => new Promise(() => undefined)), })); vi.mock('@utils/setup-utils', async (importOriginal) => ({ diff --git a/src/ui/tui/__tests__/skill-entry.test.ts b/src/ui/tui/__tests__/skill-entry.test.ts index 09f5d43a0..c12071aeb 100644 --- a/src/ui/tui/__tests__/skill-entry.test.ts +++ b/src/ui/tui/__tests__/skill-entry.test.ts @@ -1,5 +1,5 @@ import { resolveSkillEntry } from '@ui/tui/screens/SkillSourceInfo'; -import type { SkillEntry } from '@agent/tools'; +import type { SkillEntry } from '@shared/skill-menu'; const entry = (id: string): SkillEntry => ({ id, downloadUrl: `https://example.com/${id}.tar.gz` } as SkillEntry); diff --git a/src/ui/tui/screens/SkillSourceInfo.tsx b/src/ui/tui/screens/SkillSourceInfo.tsx index 7805f433c..83bf10366 100644 --- a/src/ui/tui/screens/SkillSourceInfo.tsx +++ b/src/ui/tui/screens/SkillSourceInfo.tsx @@ -14,7 +14,7 @@ import { Box, Text } from 'ink'; import { useEffect, useState } from 'react'; -import { fetchSkillMenu, type SkillEntry } from '@agent/tools'; +import { fetchSkillMenu, type SkillEntry } from '@shared/skill-menu'; import { CONTEXT_MILL_RELEASES_URL, getSkillsBaseUrl } from '@shared/constants'; /** diff --git a/src/ui/tui/screens/health/HealthCheckScreen.tsx b/src/ui/tui/screens/health/HealthCheckScreen.tsx index c19e6f5af..672113050 100644 --- a/src/ui/tui/screens/health/HealthCheckScreen.tsx +++ b/src/ui/tui/screens/health/HealthCheckScreen.tsx @@ -24,7 +24,8 @@ import { import { ServiceHealthStatus } from '@shared/health-checks/types'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; -import { fetchSkillMenu, downloadSkill } from '@agent/tools'; +import { downloadSkill } from '@agent/tools'; +import { fetchSkillMenu } from '@shared/skill-menu'; import { GITHUB_SKILLS_BASE_URL } from '@shared/constants'; import { useDismissOnAnyKey } from '@ui/tui/hooks/useDismissOnAnyKey'; From 6484ce11ce7cac2691313ab3ae120e8fefeb2ead Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 13:53:32 -0400 Subject: [PATCH 26/61] build(agent): publish the agent through entry modules `@agent` exports the runtime values callers use and `@agent/types` the types; everything outside `src/agent` imports one of the two. The MCP prompt streaming export loads on first call so the startup chunk does not grow. The architecture scanner enforces the entries on resolved paths and ESLint rejects deep `@agent/*` specifiers in the editor. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .eslintrc.cjs | 21 ++++++ README.md | 4 +- .../architecture/import-boundaries.test.ts | 72 ++++++++++++++++++- .../architecture/known-violations.json | 37 +--------- src/agent/index.ts | 38 ++++++++++ src/agent/types.ts | 30 ++++++++ src/lib/detection/agentic.ts | 4 +- src/lib/programs/agent-skill/index.ts | 2 +- src/lib/programs/audit/detect.ts | 2 +- src/lib/programs/audit/index.ts | 2 +- src/lib/programs/dispatch-family.ts | 2 +- .../detect.ts | 2 +- .../prompt.ts | 2 +- src/lib/programs/events-audit/index.ts | 2 +- src/lib/programs/mcp-analytics/index.ts | 2 +- src/lib/programs/migration/index.ts | 4 +- src/lib/programs/posthog-doctor/index.ts | 2 +- src/lib/programs/posthog-integration/index.ts | 4 +- src/lib/programs/program-run.ts | 2 +- src/lib/programs/replay-vision/index.ts | 2 +- src/lib/programs/revenue-analytics/detect.ts | 2 +- src/lib/programs/revenue-analytics/index.ts | 2 +- src/lib/programs/run-agent-legacy.ts | 18 ++--- src/lib/programs/self-driving/detect.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 | 4 +- src/lib/wizard-session.ts | 19 ++--- src/lib/wizard-spellbook.ts | 2 +- src/shared/claude-settings.ts | 3 + src/shared/errors/agent-map.ts | 3 + src/shared/errors/skill-map.ts | 2 +- src/ui/agent-progress.ts | 2 +- .../tui/screens/health/HealthCheckScreen.tsx | 2 +- .../mcp-suggested-prompts-services.ts | 4 +- src/ui/wizard-ui.ts | 2 +- tsconfig.build.json | 2 + vitest.config.ts | 2 + 40 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 src/agent/index.ts create mode 100644 src/agent/types.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 849d0ed71..77f26a9b9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -95,6 +95,27 @@ module.exports = { ], }, }, + { + // Outside the agent, import it through its entry modules only: + // `@agent` for values, `@agent/types` for types. The architecture test + // applies the same rule to resolved paths; this gives editor feedback. + files: ['bin.ts', 'src/**/*.ts', 'src/**/*.tsx'], + excludedFiles: ['src/agent/**', '**/__tests__/**', '**/__mocks__/**'], + rules: { + '@typescript-eslint/no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@agent/**', '!@agent/types'], + message: + 'Import the agent through @agent (values) or @agent/types (types).', + }, + ], + }, + ], + }, + }, { files: [ '*.test.js', diff --git a/README.md b/README.md index 8fbba68e9..b4756f113 100644 --- a/README.md +++ b/README.md @@ -504,7 +504,9 @@ Path aliases defined in `tsconfig.build.json`, resolved by tsdown: |---|---| | `@env` | `src/env.ts` | | `@lib/*` | `src/lib/*` | -| `@agent/*` | `src/agent/*` | +| `@agent` | `src/agent/index.ts`, the agent's runtime entry; the only agent import outside `src/agent` besides types | +| `@agent/types` | `src/agent/types.ts`, type-only | +| `@agent/*` | `src/agent/*`, inside `src/agent` and its tests only | | `@shared/*` | `src/shared/*` | | `@utils/*` | `src/shared/utils/*` | | `@ui/*` | `src/ui/*` | diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index dba4024a4..a7ba197ff 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -48,6 +48,28 @@ export const ALLOWED_IMPORTS: Record = { cli: ['env', 'legacy', 'agent', 'tui', 'cli'], }; +// The agent's public entries. Outside `src/agent`, an import into the agent +// must land on one of these; `types.ts` is type-only, so the TUI may take it. +const AGENT_VALUES_ENTRY = 'src/agent/index.ts'; +const AGENT_TYPES_ENTRY = 'src/agent/types.ts'; + +/** The rule an edge breaks, or null when it is allowed. */ +export function ruleFor(fromFile: string, toFile: string): string | null { + const from = classifySurface(fromFile); + const to = classifySurface(toFile); + if (to === 'agent' && from !== 'agent') { + const target = toFile.split(path.sep).join('/'); + if (target !== AGENT_VALUES_ENTRY && target !== AGENT_TYPES_ENTRY) { + return 'agent-deep-import'; + } + if (from === 'tui' && target !== AGENT_TYPES_ENTRY) { + return `matrix:${from}->${to}`; + } + return null; + } + return ALLOWED_IMPORTS[from].includes(to) ? null : `matrix:${from}->${to}`; +} + const TUI_ONLY_PACKAGES = ['ink', 'react', '@inkjs/ui', 'ink-testing-library']; const SKIP_DIRS = new Set([ @@ -267,7 +289,6 @@ function analyze(): Analysis { fs.readFileSync(path.join(REPO_ROOT, file), 'utf8'), ); const from = classifySurface(file); - const allowed = ALLOWED_IMPORTS[from]; for (const spec of specifiersIn(text)) { const base = spec.startsWith('.') @@ -293,8 +314,8 @@ function analyze(): Analysis { const key = `${file} -> ${target}`; edges.add(key); - const to = classifySurface(target); - if (!allowed.includes(to)) violations.set(key, `matrix:${from}->${to}`); + const broken = ruleFor(file, target); + if (broken !== null) violations.set(key, broken); } } @@ -392,3 +413,48 @@ describe('surface classification', () => { ).toBe('legacy'); }); }); + +describe('agent entry modules', () => { + const rule = (from: string, to: string) => ruleFor(from, to); + + it('lets legacy and cli code reach the agent through its entries only', () => { + expect(rule('src/lib/programs/audit/index.ts', 'src/agent/index.ts')).toBe( + null, + ); + expect(rule('src/lib/programs/audit/index.ts', 'src/agent/types.ts')).toBe( + null, + ); + expect(rule('src/commands/skill.ts', 'src/agent/index.ts')).toBe(null); + expect( + rule('src/lib/programs/audit/index.ts', 'src/agent/agent-runner.ts'), + ).toBe('agent-deep-import'); + expect(rule('src/commands/skill.ts', 'src/agent/runner/index.ts')).toBe( + 'agent-deep-import', + ); + expect(rule('src/shared/errors/agent-map.ts', 'src/agent/signals.ts')).toBe( + 'agent-deep-import', + ); + }); + + it('lets the TUI take agent types but not agent values', () => { + expect(rule('src/ui/tui/App.tsx', 'src/agent/types.ts')).toBe(null); + expect(rule('src/ui/tui/App.tsx', 'src/agent/index.ts')).toBe( + 'matrix:tui->agent', + ); + expect(rule('src/ui/tui/App.tsx', 'src/agent/progress.ts')).toBe( + 'agent-deep-import', + ); + }); + + it('leaves agent-internal and non-agent edges to the matrix', () => { + expect(rule('src/agent/runner/index.ts', 'src/agent/progress.ts')).toBe( + null, + ); + expect(rule('src/lib/programs/audit/index.ts', 'src/ui/tui/store.ts')).toBe( + 'matrix:legacy->tui', + ); + expect(rule('src/agent/runner/index.ts', 'src/ui/tui/store.ts')).toBe( + 'matrix:agent->tui', + ); + }); +}); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 0eecf3f85..ce41efa60 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -2,61 +2,30 @@ "violations": [ "src/commands/factories/family-picker.tsx -> src/commands/command.ts", "src/env.ts -> src/lib/headless-mode.ts", - "src/lib/detection/agentic.ts -> src/agent/agent-interface.ts", - "src/lib/programs/agent-skill/index.ts -> src/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/agent/agent-runner.ts", - "src/lib/programs/audit/index.ts -> src/agent/tools/index.ts", - "src/lib/programs/dispatch-family.ts -> src/agent/tools/index.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/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/agent/agent-interface.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/agent/tools/index.ts", - "src/lib/programs/mcp-analytics/index.ts -> src/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/agent/agent-runner.ts", - "src/lib/programs/migration/index.ts -> src/agent/tools/index.ts", "src/lib/programs/migration/index.ts -> src/lib/programs/migration/content/index.tsx", - "src/lib/programs/posthog-doctor/index.ts -> src/agent/tools/index.ts", - "src/lib/programs/posthog-integration/index.ts -> src/agent/agent-interface.ts", - "src/lib/programs/posthog-integration/index.ts -> src/agent/agent-runner.ts", - "src/lib/programs/posthog-integration/index.ts -> src/agent/tools/index.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-run.ts -> src/agent/runner/index.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/agent/agent-runner.ts", - "src/lib/programs/revenue-analytics/detect.ts -> src/agent/agent-runner.ts", - "src/lib/programs/revenue-analytics/index.ts -> src/agent/tools/index.ts", "src/lib/programs/revenue-analytics/index.ts -> src/lib/programs/revenue-analytics/content/index.tsx", - "src/lib/programs/run-agent-legacy.ts -> src/agent/agent-interface.ts", - "src/lib/programs/run-agent-legacy.ts -> src/agent/runner/index.ts", - "src/lib/programs/run-agent-legacy.ts -> src/agent/runner/switchboard/index.ts", - "src/lib/programs/run-agent-legacy.ts -> src/agent/yara-hooks.ts", - "src/lib/programs/self-driving/detect.ts -> src/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/agent/agent-interface.ts", - "src/lib/programs/self-driving/prompt.ts -> src/agent/agent-runner.ts", - "src/lib/programs/warehouse-source/detect.ts -> src/agent/agent-runner.ts", - "src/lib/programs/warehouse-source/index.ts -> src/agent/wizard-ask-bridge.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/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/agent/progress.ts", - "src/lib/wizard-spellbook.ts -> src/agent/tools/tools.ts", "src/shared/claude-settings.ts -> src/agent/agent-env-isolation.ts", "src/shared/errors/agent-map.ts -> src/agent/signals.ts", - "src/shared/errors/skill-map.ts -> src/agent/tools/index.ts", "src/shared/utils/package-manager.ts -> src/telemetry.ts", "src/shared/utils/setup-utils.ts -> src/telemetry.ts", "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts", @@ -64,10 +33,8 @@ "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", "src/steps/run-prettier.ts -> src/telemetry.ts", "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", - "src/ui/agent-progress.ts -> src/agent/progress.ts", "src/ui/index.ts -> src/ui/logging-ui.ts", - "src/ui/tui/screens/health/HealthCheckScreen.tsx -> src/agent/tools/index.ts", - "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/mcp-prompt-streaming.ts", - "src/ui/wizard-ui.ts -> src/agent/progress.ts" + "src/ui/tui/screens/health/HealthCheckScreen.tsx -> src/agent/index.ts", + "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/index.ts" ] } diff --git a/src/agent/index.ts b/src/agent/index.ts new file mode 100644 index 000000000..c014021cc --- /dev/null +++ b/src/agent/index.ts @@ -0,0 +1,38 @@ +/** + * Public entry of the agent. Code outside `src/agent` imports runtime values + * from here and types from `./types`; deeper imports fail lint and the + * architecture test. Keep this list to what callers use, and keep it cheap: + * the startup chunk imports this module, so anything re-exported here loads + * before the wizard does any work. Heavy paths stay behind a lazy import. + */ +export type * from './types'; +export { + resolveBinding, + runAgent, + RunOutcome, + shouldDisableAsk, +} from './runner'; +export { + AgentSignals, + buildRunTags, + initializeAgent, + runAgent as executeAgent, +} from './agent-interface'; +export { configureGatewayFromCIEnvironment } from './gateway-session'; +export { downloadSkill, WIZARD_TOOL_NAMES } from './tools'; +export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; +export { flushScanReport } from './yara-hooks'; + +/** + * Stream an MCP tutorial prompt through the SDK. Loaded on first call: the + * streaming module is the one agent module the startup chunk does not already + * hold, and the TUI reaches it only from the suggested-prompts screen. + */ +export async function* runMcpPromptViaSdk( + args: Parameters< + typeof import('./mcp-prompt-streaming').runMcpPromptViaSdk + >[0], +): AsyncIterable { + const streaming = await import('./mcp-prompt-streaming'); + yield* streaming.runMcpPromptViaSdk(args); +} diff --git a/src/agent/types.ts b/src/agent/types.ts new file mode 100644 index 000000000..6b42c298a --- /dev/null +++ b/src/agent/types.ts @@ -0,0 +1,30 @@ +/** + * Public type surface of the agent. Type-only, so importing it adds no + * runtime dependency. Code outside `src/agent` imports these as + * `@agent/types`; runtime values come from `@agent`. + */ +export type { + AbortCase, + AgentFailure, + AgentRunDefinition, + ProgramBinding, + PromptContext, + RunConfig, + RunInput, + RunResult, + SwitchboardCtx, +} from './runner'; +export type { + AgentInteraction, + AgentProgress, + AskAnswers, + AskQuestion, + AuthErrorDetail, + OutroData, + PendingQuestion, + SpinnerHandle, + TaskNotice, + TokenUsageDelta, +} from './progress'; +export type { AgentChunk } from './mcp-prompt-streaming'; +export type { InstallSkillResult } from './tools'; diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index c2ce6a279..e3f96aa22 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -15,10 +15,10 @@ import { initializeAgent, - runAgent as executeAgent, + executeAgent, buildRunTags, AgentSignals, -} from '@agent/agent-interface'; +} from '@agent'; import { isAbsolute, resolve, sep } from 'path'; import { detectNodePackageManagers } from './package-manager.js'; import { CallType, getSkillsBaseUrl, HAIKU_MODEL } from '@shared/constants'; diff --git a/src/lib/programs/agent-skill/index.ts b/src/lib/programs/agent-skill/index.ts index 8ab764911..e59f3fdf1 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 { AbortCase } from '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import type { ProgramRun } from '@lib/programs/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 37ee475b2..253c409e6 100644 --- a/src/lib/programs/audit/detect.ts +++ b/src/lib/programs/audit/detect.ts @@ -1,4 +1,4 @@ -import type { AbortCase } from '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/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 110f09816..a5921edbe 100644 --- a/src/lib/programs/audit/index.ts +++ b/src/lib/programs/audit/index.ts @@ -6,7 +6,7 @@ import type { ProgramStep, ProgramConfig } from '@lib/programs/program-step'; import type { ProgramRun } from '@lib/programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import { WIZARD_TOOL_NAMES } from '@agent'; import { headlessOption, regionOption } from '@lib/headless-mode'; import { AUDIT_ABORT_CASES } from './detect.js'; import { diff --git a/src/lib/programs/dispatch-family.ts b/src/lib/programs/dispatch-family.ts index f2922077b..b9da9d39f 100644 --- a/src/lib/programs/dispatch-family.ts +++ b/src/lib/programs/dispatch-family.ts @@ -2,7 +2,7 @@ import type { Arguments } from 'yargs'; import { auditConfig } from '@lib/programs/audit/index'; import { AUDIT_CHECKS_FILE } from '@lib/programs/audit/types'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import { WIZARD_TOOL_NAMES } from '@agent'; import { agentSkillConfig } from '@lib/programs/program-registry'; import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; import type { ProgramConfig } from '@lib/programs/program-step'; 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 15ff114af..3574ea1b9 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; /** 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 df2539095..fbc44f554 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 '@agent/agent-interface'; +import { AgentSignals } from '@agent'; import type { SkillVariant } from './detect.js'; export type SourceMapsUploadPromptParams = { diff --git a/src/lib/programs/events-audit/index.ts b/src/lib/programs/events-audit/index.ts index 850ae62af..d072c7f60 100644 --- a/src/lib/programs/events-audit/index.ts +++ b/src/lib/programs/events-audit/index.ts @@ -4,7 +4,7 @@ import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { SPINNER_MESSAGE } from '@lib/framework-config'; import { isUsingTypeScript } from '@utils/setup-utils'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import { WIZARD_TOOL_NAMES } from '@agent'; import { EVENTS_AUDIT_PROGRAM } from './steps.js'; import { AUDIT_CHECKS_FILE, AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; import { seedAuditLedger } from '@lib/programs/audit/seed'; diff --git a/src/lib/programs/mcp-analytics/index.ts b/src/lib/programs/mcp-analytics/index.ts index 4b62d407b..458ba18e7 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/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 2fc157746..173e007be 100644 --- a/src/lib/programs/migration/index.ts +++ b/src/lib/programs/migration/index.ts @@ -1,6 +1,6 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import type { AbortCase } from '@agent/agent-runner'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import type { AbortCase } from '@agent/types'; +import { WIZARD_TOOL_NAMES } from '@agent'; import { MIGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/posthog-doctor/index.ts b/src/lib/programs/posthog-doctor/index.ts index cf57eb9c7..a6ad9f10e 100644 --- a/src/lib/programs/posthog-doctor/index.ts +++ b/src/lib/programs/posthog-doctor/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import { WIZARD_TOOL_NAMES } from '@agent'; import { POSTHOG_DOCTOR_PROGRAM } from './steps.js'; export const posthogDoctorConfig: ProgramConfig = { diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index 5a04cc46d..4a6a04a2b 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -1,10 +1,9 @@ import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; import { runProgramAgent } from '@lib/programs/run-agent-legacy'; import type { ProgramRun } from '@lib/programs/program-run'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import { AgentSignals, shouldDisableAsk, WIZARD_TOOL_NAMES } from '@agent'; import type { WizardSession } from '@lib/wizard-session'; import { mayReportScanResults, OutroKind, RunPhase } from '@lib/wizard-session'; -import { AgentSignals } from '@agent/agent-interface'; import { DEFAULT_PACKAGE_INSTALLATION, SPINNER_MESSAGE, @@ -22,7 +21,6 @@ import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; import type { HostResolution } from '@shared/host-resolution'; import { getDetectedWarehouseSources } from '@lib/programs/warehouse-source/detect'; -import { shouldDisableAsk } from '@agent/agent-runner'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from './content/index.js'; import { buildCodingAgentPrompt } from './handoff.js'; diff --git a/src/lib/programs/program-run.ts b/src/lib/programs/program-run.ts index 523d3bbbe..a3d9a15be 100644 --- a/src/lib/programs/program-run.ts +++ b/src/lib/programs/program-run.ts @@ -5,7 +5,7 @@ * agent `RunConfig.hooks`. */ -import type { AgentRunDefinition } from '@agent/runner'; +import type { AgentRunDefinition } from '@agent/types'; import type { Credentials, WizardSession } from '@lib/wizard-session'; export interface ProgramRun extends AgentRunDefinition { diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index 637f162f5..ef01e6a9d 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { Integration } from '@shared/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 7089e6e51..d63ef3c6a 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { findPackageJsons } from '@lib/programs/shared/package-scanning'; export { diff --git a/src/lib/programs/revenue-analytics/index.ts b/src/lib/programs/revenue-analytics/index.ts index 9194f9e70..fade6e3fc 100644 --- a/src/lib/programs/revenue-analytics/index.ts +++ b/src/lib/programs/revenue-analytics/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig } from '@lib/programs/program-step'; -import { WIZARD_TOOL_NAMES } from '@agent/tools'; +import { WIZARD_TOOL_NAMES } from '@agent'; import { REVENUE_ANALYTICS_PROGRAM } from './steps.js'; import { REVENUE_ABORT_CASES } from './detect.js'; import { getContentBlocks } from './content/index.js'; diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index f259533d6..e0c0733d0 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -18,23 +18,25 @@ import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; import { createUiReducer, uiInteraction } from '@ui/agent-progress'; import { + buildRunTags, + flushScanReport, + resolveBinding, runAgent, RunOutcome, - resolveBinding, - type RunConfig, - type RunInput, - type SwitchboardCtx, -} from '@agent/runner'; -import type { ProgramBinding } from '@agent/runner/switchboard'; +} from '@agent'; +import type { + ProgramBinding, + RunConfig, + RunInput, + SwitchboardCtx, +} from '@agent/types'; import type { ProgramRun } from './program-run'; -import { buildRunTags } from '@agent/agent-interface'; import { backupAndFixClaudeSettings, checkAllSettingsConflicts, classifySettingsConflicts, restoreClaudeSettings, } from '@shared/claude-settings'; -import { flushScanReport } from '@agent/yara-hooks'; import { evaluateWizardReadiness, WizardReadiness, diff --git a/src/lib/programs/self-driving/detect.ts b/src/lib/programs/self-driving/detect.ts index 080b4ce58..a5dbdb546 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import type { DetectedSource } from '@lib/warehouse-sources/types'; diff --git a/src/lib/programs/self-driving/prompt.ts b/src/lib/programs/self-driving/prompt.ts index b79eeaaf9..db629ea28 100644 --- a/src/lib/programs/self-driving/prompt.ts +++ b/src/lib/programs/self-driving/prompt.ts @@ -1,5 +1,5 @@ -import { AgentSignals } from '@agent/agent-interface'; -import type { PromptContext } from '@agent/agent-runner'; +import { AgentSignals } from '@agent'; +import type { PromptContext } from '@agent/types'; 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 c9cfa9673..22a7b79ab 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; 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 7b5e99c23..a2fb7db06 100644 --- a/src/lib/programs/warehouse-source/index.ts +++ b/src/lib/programs/warehouse-source/index.ts @@ -1,7 +1,7 @@ import type { ProgramConfig } from '@lib/programs/program-step'; import type { ProgramRun } from '@lib/programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; -import { LONGER_ASK_TIMEOUT_MS } from '@agent/wizard-ask-bridge'; +import { LONGER_ASK_TIMEOUT_MS } from '@agent'; import { WAREHOUSE_SOURCE_PROGRAM } from './steps.js'; import { WAREHOUSE_ABORT_CASES, diff --git a/src/lib/programs/web-analytics-doctor/detect.ts b/src/lib/programs/web-analytics-doctor/detect.ts index 30ab8ce01..9a05bbc00 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 '@agent/agent-runner'; +import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/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 dfa32f9c7..740d102b9 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -242,9 +242,7 @@ export function runNonInteractive( try { if (mode === 'ci') { - const { configureGatewayFromCIEnvironment } = await import( - '@agent/gateway-session' - ); + const { configureGatewayFromCIEnvironment } = await import('@agent'); configureGatewayFromCIEnvironment( Number(session.projectId), session.region ?? 'us', diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 18edf4972..e35ba2325 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -24,14 +24,17 @@ import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { SettingsConflict } from '@shared/claude-settings'; import type { ApiUser, ApiProject, Credentials } from '@shared/api'; import type { CloudRegion } from '@utils/types'; -import { - OutroKind, - type AskAnswers, - type AskQuestion, - type OutroData, - type PendingQuestion, - type TaskNotice, -} from '@agent/progress'; +import type { + AskAnswers, + AskQuestion, + OutroData, + PendingQuestion, + TaskNotice, +} from '@agent/types'; +// Leaf module on purpose: shared analytics imports this file, so the agent +// entry would form a module cycle here. +// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B2: the session becomes a TUI projection +import { OutroKind } from '@agent/progress'; // These shapes moved to their owners; re-exported so every session reader // keeps its import path. `Credentials` sits with the API types, the diff --git a/src/lib/wizard-spellbook.ts b/src/lib/wizard-spellbook.ts index be7105e51..8473d8096 100644 --- a/src/lib/wizard-spellbook.ts +++ b/src/lib/wizard-spellbook.ts @@ -3,7 +3,7 @@ import path from 'path'; import { getSkillsBaseUrl, POSTHOG_DOCS_URL } from '@shared/constants'; import type { ProgramConfig } from './programs/program-step'; import type { WizardSession } from './wizard-session'; -import { downloadSkill } from '@agent/tools/tools'; +import { downloadSkill } from '@agent'; import { fetchSkillMenu, type SkillEntry, diff --git a/src/shared/claude-settings.ts b/src/shared/claude-settings.ts index da4873eec..05d697273 100644 --- a/src/shared/claude-settings.ts +++ b/src/shared/claude-settings.ts @@ -12,6 +12,9 @@ import * as fs from 'fs'; import * as os from 'os'; import { analytics } from '@utils/analytics'; import { registerCleanup } from '@utils/wizard-abort'; +// Leaf module on purpose: shared analytics imports the session, which this +// file feeds, so the agent entry would form a module cycle here. +// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B1: shared stops feeding the session import { BLOCKED_AGENT_ENV_KEYS, BLOCKED_AGENT_ENV_PATTERNS, diff --git a/src/shared/errors/agent-map.ts b/src/shared/errors/agent-map.ts index c1160d14b..e897b5085 100644 --- a/src/shared/errors/agent-map.ts +++ b/src/shared/errors/agent-map.ts @@ -1,3 +1,6 @@ +// Leaf module on purpose: every module imports the errors, so the agent entry +// would form a module cycle here. +// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B1: error codes move next to their owners import { AgentErrorType } from '@agent/signals'; import { ErrorCodes, type ErrorCode } from './codes'; diff --git a/src/shared/errors/skill-map.ts b/src/shared/errors/skill-map.ts index 50f9851fb..97d1a1419 100644 --- a/src/shared/errors/skill-map.ts +++ b/src/shared/errors/skill-map.ts @@ -1,5 +1,5 @@ import { ErrorCodes, type ErrorCode } from './codes'; -import type { InstallSkillResult } from '@agent/tools'; +import type { InstallSkillResult } from '@agent/types'; const SKILL_CODES: Record< Exclude, diff --git a/src/ui/agent-progress.ts b/src/ui/agent-progress.ts index a28dcbf27..93e6df292 100644 --- a/src/ui/agent-progress.ts +++ b/src/ui/agent-progress.ts @@ -1,5 +1,5 @@ import type { WizardUI, SpinnerHandle } from './wizard-ui'; -import type { AgentInteraction, AgentProgress } from '@agent/progress'; +import type { AgentInteraction, AgentProgress } from '@agent/types'; // ── Progress → WizardUI, one call per event ─────────────────────────── diff --git a/src/ui/tui/screens/health/HealthCheckScreen.tsx b/src/ui/tui/screens/health/HealthCheckScreen.tsx index 672113050..676276935 100644 --- a/src/ui/tui/screens/health/HealthCheckScreen.tsx +++ b/src/ui/tui/screens/health/HealthCheckScreen.tsx @@ -24,7 +24,7 @@ import { import { ServiceHealthStatus } from '@shared/health-checks/types'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; -import { downloadSkill } from '@agent/tools'; +import { downloadSkill } from '@agent'; import { fetchSkillMenu } from '@shared/skill-menu'; import { GITHUB_SKILLS_BASE_URL } from '@shared/constants'; import { useDismissOnAnyKey } from '@ui/tui/hooks/useDismissOnAnyKey'; diff --git a/src/ui/tui/services/mcp-suggested-prompts-services.ts b/src/ui/tui/services/mcp-suggested-prompts-services.ts index 8651f8dc9..b92ad2d16 100644 --- a/src/ui/tui/services/mcp-suggested-prompts-services.ts +++ b/src/ui/tui/services/mcp-suggested-prompts-services.ts @@ -23,7 +23,7 @@ import { seedDemoEvents as runSeed } from '@lib/mcp-seed-events'; // The streamed event shape is the agent's; re-exported so the screen and the // playground keep their import path. -import type { AgentChunk } from '@agent/mcp-prompt-streaming'; +import type { AgentChunk } from '@agent/types'; export type { AgentChunk }; export interface McpSuggestedPromptsServices { @@ -161,6 +161,6 @@ async function* runProductionPromptStreaming(args: { // Defer the SDK import to call time — the playground never hits // this path (it overrides the whole service object), so demo // sessions don't pay the SDK load cost. - const { runMcpPromptViaSdk } = await import('@agent/mcp-prompt-streaming'); + const { runMcpPromptViaSdk } = await import('@agent'); yield* runMcpPromptViaSdk(args); } diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts index f99802402..e972a5063 100644 --- a/src/ui/wizard-ui.ts +++ b/src/ui/wizard-ui.ts @@ -34,7 +34,7 @@ import type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta, -} from '@agent/progress'; +} from '@agent/types'; export type { AuthErrorDetail, SpinnerHandle, TokenUsageDelta }; export interface WizardUI { diff --git a/tsconfig.build.json b/tsconfig.build.json index f4c4ef2d9..d716e8367 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -20,6 +20,8 @@ "paths": { "@env": ["./src/env.ts"], "@shared/*": ["./src/shared/*"], + "@agent": ["./src/agent/index.ts"], + "@agent/types": ["./src/agent/types.ts"], "@agent/*": ["./src/agent/*"], "@lib/*": ["./src/lib/*"], "@e2e-harness/*": ["./e2e-harness/*"], diff --git a/vitest.config.ts b/vitest.config.ts index 14d4149f8..713b61ac1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -79,6 +79,8 @@ export default defineConfig({ }, { find: /^ink$/, replacement: r('__mocks__/ink.ts') }, { find: /^@shared\/(.*)$/, replacement: `${r('src/shared')}/$1` }, + { find: /^@agent$/, replacement: r('src/agent/index.ts') }, + { find: /^@agent\/types$/, replacement: r('src/agent/types.ts') }, { find: /^@agent\/(.*)$/, replacement: `${r('src/agent')}/$1` }, // Path aliases — mirror tsconfig `paths`. { find: /^@env$/, replacement: r('src/env.ts') }, From 9f0d4c443bd2eff3940cf6251f419ff7fca899e8 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 13:59:06 -0400 Subject: [PATCH 27/61] refactor(shared): stop shared code from reaching upward `debug()` reports through an injected sink that the UI module installs, so shared no longer looks the UI up. The progress tag helpers move from `src/telemetry.ts` to `@utils/telemetry` and the detect error map moves to programs, which own the detect error kinds. `src/shared` is now its own surface in the architecture test; the remaining upward edges are listed with their owners in the stack plan. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- docs/error-catalog.md | 2 +- .../architecture/import-boundaries.test.ts | 17 +++++---- .../architecture/known-violations.json | 14 ++++---- src/__tests__/cli.test.ts | 1 + src/__tests__/provision-cli.test.ts | 1 + src/agent/__tests__/gateway-session.test.ts | 2 +- .../programs}/__tests__/detect-map.test.ts | 3 +- .../errors => lib/programs}/detect-map.ts | 2 +- src/lib/runners/run-non-interactive.ts | 2 +- src/shared/errors/index.ts | 1 - src/shared/utils/__tests__/debug.test.ts | 35 +++++++++++++++++++ .../utils/__tests__/oauth-refresh.test.ts | 2 +- .../utils/__tests__/oauth-server.test.ts | 2 +- src/shared/utils/debug.ts | 15 ++++++-- src/shared/utils/package-manager.ts | 2 +- src/shared/utils/setup-utils.ts | 2 +- src/{ => shared/utils}/telemetry.ts | 2 +- src/steps/add-mcp-server-to-clients/index.ts | 2 +- .../add-or-update-environment-variables.ts | 2 +- src/steps/run-prettier.ts | 2 +- .../upload-environment-variables/index.ts | 2 +- src/ui/index.ts | 5 +++ 22 files changed, 87 insertions(+), 31 deletions(-) rename src/{shared/errors => lib/programs}/__tests__/detect-map.test.ts (96%) rename src/{shared/errors => lib/programs}/detect-map.ts (97%) rename src/{ => shared/utils}/telemetry.ts (81%) diff --git a/docs/error-catalog.md b/docs/error-catalog.md index 8e8c05b19..c162e2897 100644 --- a/docs/error-catalog.md +++ b/docs/error-catalog.md @@ -117,7 +117,7 @@ guarantee. Program detect steps write `{ kind, ...detail }` into `session.frameworkContext.detectError`. `detectErrorCode()` -([`src/shared/errors/detect-map.ts`](../src/shared/errors/detect-map.ts)) maps `kind` +([`src/lib/programs/detect-map.ts`](../src/lib/programs/detect-map.ts)) maps `kind` → code, and the whole object — `kind` included — rides along as `OutroData.errorDetail`. diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index a7ba197ff..9ba7d9236 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; -export type Surface = 'env' | 'legacy' | 'agent' | 'tui' | 'cli'; +export type Surface = 'env' | 'shared' | 'legacy' | 'agent' | 'tui' | 'cli'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(HERE, '../../..'); @@ -10,6 +10,7 @@ const REPO_ROOT = path.resolve(HERE, '../../..'); const SURFACE_RULES: ReadonlyArray boolean]> = [ ['env', (p) => p === 'src/env.ts'], + ['shared', (p) => p.startsWith('src/shared/')], ['agent', (p) => p.startsWith('src/agent/')], [ 'tui', @@ -26,7 +27,6 @@ const SURFACE_RULES: ReadonlyArray boolean]> = (p) => p === 'bin.ts' || p === 'src/wizard.ts' || - p === 'src/telemetry.ts' || p.startsWith('src/commands/') || p.startsWith('src/lib/runners/'), ], @@ -42,10 +42,12 @@ export function classifySurface(relPath: string): Surface { export const ALLOWED_IMPORTS: Record = { env: [], - legacy: ['env', 'legacy'], - agent: ['env', 'legacy', 'agent'], - tui: ['env', 'legacy', 'tui'], - cli: ['env', 'legacy', 'agent', 'tui', 'cli'], + shared: ['env', 'shared'], + legacy: ['env', 'shared', 'legacy'], + // Program types stay importable from the agent until B1 moves the bindings. + agent: ['env', 'shared', 'legacy', 'agent'], + tui: ['env', 'shared', 'legacy', 'tui'], + cli: ['env', 'shared', 'legacy', 'agent', 'tui', 'cli'], }; // The agent's public entries. Outside `src/agent`, an import into the agent @@ -396,7 +398,8 @@ describe('import boundaries', () => { describe('surface classification', () => { it('maps representative paths to their surface', () => { expect(classifySurface('src/env.ts')).toBe('env'); - expect(classifySurface('src/shared/utils/analytics.ts')).toBe('legacy'); + expect(classifySurface('src/shared/utils/analytics.ts')).toBe('shared'); + expect(classifySurface('src/shared/errors/codes.ts')).toBe('shared'); expect(classifySurface('src/agent/agent-runner.ts')).toBe('agent'); expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); expect(classifySurface('bin.ts')).toBe('cli'); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index ce41efa60..629085454 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -26,13 +26,15 @@ "src/lib/wizard-session.ts -> src/agent/progress.ts", "src/shared/claude-settings.ts -> src/agent/agent-env-isolation.ts", "src/shared/errors/agent-map.ts -> src/agent/signals.ts", - "src/shared/utils/package-manager.ts -> src/telemetry.ts", - "src/shared/utils/setup-utils.ts -> src/telemetry.ts", + "src/shared/utils/analytics.ts -> src/lib/wizard-session.ts", + "src/shared/utils/oauth.ts -> src/ui/index.ts", + "src/shared/utils/setup-utils.ts -> src/lib/oauth/program-scopes.ts", + "src/shared/utils/setup-utils.ts -> src/lib/programs/program-registry.ts", + "src/shared/utils/setup-utils.ts -> src/lib/wizard-session.ts", + "src/shared/utils/setup-utils.ts -> src/ui/index.ts", + "src/shared/utils/wizard-abort.ts -> src/lib/wizard-session.ts", + "src/shared/utils/wizard-abort.ts -> src/ui/index.ts", "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts", - "src/steps/add-mcp-server-to-clients/index.ts -> src/telemetry.ts", - "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", - "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/tui/screens/health/HealthCheckScreen.tsx -> src/agent/index.ts", "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/index.ts" diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index aedc37a40..73de7fa8a 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -99,6 +99,7 @@ vi.mock('@utils/env-api-key', () => ({ vi.mock('@utils/debug', () => ({ configureLogFileFromEnvironment: vi.fn(), logToFile: vi.fn(), + setDebugSink: vi.fn(), })); vi.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); vi.mock('../lib/detection/index', () => ({ diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 636f75f72..94b76cb25 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -50,6 +50,7 @@ vi.mock('@utils/env-api-key', () => ({ vi.mock('@utils/debug', () => ({ configureLogFileFromEnvironment: vi.fn(), logToFile: vi.fn(), + setDebugSink: vi.fn(), })); vi.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); vi.mock('../lib/detection/index', () => ({ diff --git a/src/agent/__tests__/gateway-session.test.ts b/src/agent/__tests__/gateway-session.test.ts index 23d5562d8..cf1eda731 100644 --- a/src/agent/__tests__/gateway-session.test.ts +++ b/src/agent/__tests__/gateway-session.test.ts @@ -26,7 +26,7 @@ vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn(), captureException: vi.fn() }, })); -vi.mock('@utils/debug', () => ({ logToFile: vi.fn() })); +vi.mock('@utils/debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn() })); // logToFile is variadic, so a leak in any argument is a leak. Rendered every way the // sink might: JSON (which invokes getters and toJSON), an Error's stack, and inspect. diff --git a/src/shared/errors/__tests__/detect-map.test.ts b/src/lib/programs/__tests__/detect-map.test.ts similarity index 96% rename from src/shared/errors/__tests__/detect-map.test.ts rename to src/lib/programs/__tests__/detect-map.test.ts index 28d12cac7..b01368a1a 100644 --- a/src/shared/errors/__tests__/detect-map.test.ts +++ b/src/lib/programs/__tests__/detect-map.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { ErrorCodes } from '../codes'; -import { ERROR_CATALOG } from '../catalog'; +import { ErrorCodes, ERROR_CATALOG } from '@shared/errors'; import { detectErrorCode, type DetectErrorKind } from '../detect-map'; /** diff --git a/src/shared/errors/detect-map.ts b/src/lib/programs/detect-map.ts similarity index 97% rename from src/shared/errors/detect-map.ts rename to src/lib/programs/detect-map.ts index b632966ac..aaf4e25e2 100644 --- a/src/shared/errors/detect-map.ts +++ b/src/lib/programs/detect-map.ts @@ -1,4 +1,4 @@ -import { ErrorCodes, type ErrorCode } from './codes'; +import { ErrorCodes, type ErrorCode } from '@shared/errors'; import type { RevenueDetectError } from '@lib/programs/revenue-analytics/detect'; import type { SelfDrivingDetectError } from '@lib/programs/self-driving/detect'; import type { SourceMapsDetectError } from '@lib/programs/error-tracking-upload-source-maps/detect'; diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 740d102b9..6977ae5f9 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -21,9 +21,9 @@ import { join } from 'node:path'; import { ErrorCodes, classifyRunFailure, - detectErrorCode, emitWizardError, } from '@shared/errors'; +import { detectErrorCode } from '@lib/programs/detect-map'; import type { OutroData, RunPhase as RunPhaseT } from '@lib/wizard-session'; /** diff --git a/src/shared/errors/index.ts b/src/shared/errors/index.ts index 51a0793d7..67178ff72 100644 --- a/src/shared/errors/index.ts +++ b/src/shared/errors/index.ts @@ -9,7 +9,6 @@ export { WizardError } from './wizard-error'; export type { ErrorCatalogEntry, ErrorGroup, RetryAdvice } from './types'; export { classifyAuthFailure, type AuthFailureInput } from './auth'; export { AGENT_ERROR_CODE } from './agent-map'; -export { detectErrorCode, type DetectErrorKind } from './detect-map'; export { skillErrorCode } from './skill-map'; export { PHW_ERROR_PREFIX, diff --git a/src/shared/utils/__tests__/debug.test.ts b/src/shared/utils/__tests__/debug.test.ts index 31ab68581..05cec1309 100644 --- a/src/shared/utils/__tests__/debug.test.ts +++ b/src/shared/utils/__tests__/debug.test.ts @@ -64,3 +64,38 @@ describe('log file writing', () => { expect(content).toContain('second write'); }); }); + +describe('debug console sink', () => { + it('sends enabled debug lines to the injected sink only', async () => { + const { debug, enableDebugLogs, setDebugSink } = await import('../debug'); + const lines: string[] = []; + const previous = setDebugSink((line) => lines.push(line)); + try { + debug('before enable'); + expect(lines).toEqual([]); + enableDebugLogs(); + debug('hello', 'world'); + expect(lines).toEqual(['hello world']); + } finally { + setDebugSink(previous); + } + }); + + it('is wired to the current UI by the UI module', async () => { + const { debug, enableDebugLogs } = await import('../debug'); + const { getUI, setUI } = await import('@ui'); + const seen: string[] = []; + const original = getUI(); + setUI({ + ...original, + log: { ...original.log, info: (line: string) => seen.push(line) }, + } as typeof original); + try { + enableDebugLogs(); + debug('routed'); + expect(seen).toEqual(['routed']); + } finally { + setUI(original); + } + }); +}); diff --git a/src/shared/utils/__tests__/oauth-refresh.test.ts b/src/shared/utils/__tests__/oauth-refresh.test.ts index d3fafdd39..525ea9400 100644 --- a/src/shared/utils/__tests__/oauth-refresh.test.ts +++ b/src/shared/utils/__tests__/oauth-refresh.test.ts @@ -8,7 +8,7 @@ vi.mock('../urls', async (importOriginal) => ({ ...(await importOriginal()), resolveBaseUrl: (baseUrl?: string) => baseUrl, })); -vi.mock('../debug', () => ({ logToFile: vi.fn() })); +vi.mock('../debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn() })); const mockedAxios = axios as Mocked; diff --git a/src/shared/utils/__tests__/oauth-server.test.ts b/src/shared/utils/__tests__/oauth-server.test.ts index f5086fa04..87c0dc8df 100644 --- a/src/shared/utils/__tests__/oauth-server.test.ts +++ b/src/shared/utils/__tests__/oauth-server.test.ts @@ -3,7 +3,7 @@ import * as net from 'node:net'; import { startCallbackServer } from '@utils/oauth'; import { logToFile } from '../debug'; -vi.mock('../debug', () => ({ logToFile: vi.fn() })); +vi.mock('../debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn() })); const authUrl = 'https://oauth.example.test/authorize'; const signupUrl = 'https://oauth.example.test/signup'; diff --git a/src/shared/utils/debug.ts b/src/shared/utils/debug.ts index 1081ff09a..807d2639a 100644 --- a/src/shared/utils/debug.ts +++ b/src/shared/utils/debug.ts @@ -1,7 +1,6 @@ import { appendFileSync, mkdirSync } from 'fs'; import path from 'path'; import { inspect } from 'node:util'; -import { getUI } from '@ui'; import { IS_DEV, runtimeEnv } from '@env'; import { WIZARD_LOG_FILE } from './paths'; @@ -107,9 +106,21 @@ export function logToFile(...args: unknown[]): void { appendLine(`[${ts}] ${renderLine(args)}\n`); } +/** Where `debug()` lines go. The UI module installs the current UI's info log at load; until then they go to stdout. */ +export type DebugSink = (line: string) => void; + +let debugSink: DebugSink = (line) => process.stdout.write(`${line}\n`); + +/** Replace the console sink; returns the previous one so callers can restore it. */ +export function setDebugSink(sink: DebugSink): DebugSink { + const previous = debugSink; + debugSink = sink; + return previous; +} + export function debug(...args: unknown[]): void { if (!consoleLoggingEnabled) return; - getUI().log.info(renderLine(args)); + debugSink(renderLine(args)); } export function enableDebugLogs(): void { diff --git a/src/shared/utils/package-manager.ts b/src/shared/utils/package-manager.ts index dc846f275..cd95772e1 100644 --- a/src/shared/utils/package-manager.ts +++ b/src/shared/utils/package-manager.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { readFileHead } from './bounded-fs'; -import { withProgress } from '../../telemetry'; +import { withProgress } from './telemetry'; import { getPackageDotJson, updatePackageDotJson } from './setup-utils'; import type { PackageJson } from './package-json'; import { analytics } from './analytics'; diff --git a/src/shared/utils/setup-utils.ts b/src/shared/utils/setup-utils.ts index eb434f8ef..f8857b555 100644 --- a/src/shared/utils/setup-utils.ts +++ b/src/shared/utils/setup-utils.ts @@ -4,7 +4,7 @@ import * as os from 'node:os'; import { basename, isAbsolute, join, relative } from 'node:path'; import { promisify } from 'node:util'; -import { withProgress } from '../../telemetry'; +import { withProgress } from './telemetry'; import { debug, logToFile } from './debug'; import type { PackageJson } from './package-json'; import { diff --git a/src/telemetry.ts b/src/shared/utils/telemetry.ts similarity index 81% rename from src/telemetry.ts rename to src/shared/utils/telemetry.ts index 5d4bc955a..ba641632c 100644 --- a/src/telemetry.ts +++ b/src/shared/utils/telemetry.ts @@ -1,4 +1,4 @@ -import { analytics } from '@utils/analytics'; +import { analytics } from './analytics'; export function withProgress(step: string, callback: () => T): T { updateProgress(step); diff --git a/src/steps/add-mcp-server-to-clients/index.ts b/src/steps/add-mcp-server-to-clients/index.ts index fcc85a83b..ec014851f 100644 --- a/src/steps/add-mcp-server-to-clients/index.ts +++ b/src/steps/add-mcp-server-to-clients/index.ts @@ -1,6 +1,6 @@ import type { Integration } from '@shared/constants'; import type { CloudRegion } from '@utils/types'; -import { withProgress } from '../../telemetry'; +import { withProgress } from '@utils/telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; import { MCPClient } from './MCPClient'; diff --git a/src/steps/add-or-update-environment-variables.ts b/src/steps/add-or-update-environment-variables.ts index f4d5d20d4..05c8a3579 100644 --- a/src/steps/add-or-update-environment-variables.ts +++ b/src/steps/add-or-update-environment-variables.ts @@ -1,5 +1,5 @@ import type { Integration } from '@shared/constants'; -import { withProgress } from '../telemetry'; +import { withProgress } from '@utils/telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; import { getDotGitignore } from '@utils/bounded-fs'; diff --git a/src/steps/run-prettier.ts b/src/steps/run-prettier.ts index 95078bdd9..33262dd53 100644 --- a/src/steps/run-prettier.ts +++ b/src/steps/run-prettier.ts @@ -1,5 +1,5 @@ import type { Integration } from '@shared/constants'; -import { withProgress } from '../telemetry'; +import { withProgress } from '@utils/telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; import { diff --git a/src/steps/upload-environment-variables/index.ts b/src/steps/upload-environment-variables/index.ts index 239287573..51b61bc22 100644 --- a/src/steps/upload-environment-variables/index.ts +++ b/src/steps/upload-environment-variables/index.ts @@ -1,5 +1,5 @@ import type { Integration } from '@shared/constants'; -import { withProgress } from '../../telemetry'; +import { withProgress } from '@utils/telemetry'; import { analytics } from '@utils/analytics'; import { getUI } from '@ui'; import type { WizardSession } from '@lib/wizard-session'; diff --git a/src/ui/index.ts b/src/ui/index.ts index 9688eaf6f..dc374144a 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -5,9 +5,14 @@ import type { WizardUI } from './wizard-ui'; import { LoggingUI } from './logging-ui'; +import { setDebugSink } from '@utils/debug'; let currentUI: WizardUI = new LoggingUI(); +// Shared code never looks the UI up; `debug()` reports through whichever UI +// is current, installed here so the sink follows setUI(). +setDebugSink((line) => currentUI.log.info(line)); + export function getUI(): WizardUI { return currentUI; } From 49614fb372650410ce93fa27fdec02f323447963 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 14:01:49 -0400 Subject: [PATCH 28/61] docs: describe the agent and shared surfaces Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- AGENTS.md | 9 +++ src/agent/README.md | 69 ++++++++++++++++++++ src/agent/runner/README.md | 2 +- src/agent/runner/harness/anthropic/README.md | 6 +- src/agent/runner/sequence/README.md | 2 +- src/shared/README.md | 43 ++++++++++++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/agent/README.md create mode 100644 src/shared/README.md diff --git a/AGENTS.md b/AGENTS.md index bf6620e5c..ad66039db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,10 @@ Each domain has a dedicated boundary: [ai-gateway](https://github.com/PostHog/ai-gateway). To disable scanning in the field without a release, see the kill-switch runbook: `docs/runbooks/warlock-kill-switch.md`. ONLY USE THIS IF ABSOLUTELY NECESSARY. +- **Agent** → `src/agent/`, imported only through `@agent` (values) and + `@agent/types` (types); see [src/agent/README.md](src/agent/README.md) +- **Shared** → `src/shared/`, stateless library code with no upward imports; + see [src/shared/README.md](src/shared/README.md) - **Programs** → step arrays in `src/lib/programs/` - **TUI** → screen components and primitives in `src/ui/tui/` @@ -197,6 +201,11 @@ wizard run points. Full catalog: [`docs/local-dev.md`](docs/local-dev.md). types so they satisfy `Record`. - All UI calls go through `getUI()` (returns `WizardUI` interface). Never import the store directly from business logic. +- Shared helpers never call `getUI()`; they take a sink or return data. `debug()` + reaches the UI through the sink `src/ui/index.ts` installs. +- Outside `src/agent`, import the agent through `@agent` or `@agent/types`. Add + to those entry modules rather than deep-importing; lint and + `pnpm test:arch` reject `@agent/*` paths elsewhere. - Session mutations go through explicit store setters that call `emitChange()`. Never mutate `session` directly — nanostore holds a shallow copy. - The router resolves the active screen from session state. No imperative diff --git a/src/agent/README.md b/src/agent/README.md new file mode 100644 index 000000000..1c86b2e08 --- /dev/null +++ b/src/agent/README.md @@ -0,0 +1,69 @@ +# Agent + +The agent runs one program's AI pipeline against a project directory. It takes resolved data in, reports through progress events, asks through an injected answerer, and returns a result. It never reads a session, a store or a UI. + +## Signatures + +Import runtime values from `@agent` and types from `@agent/types`. Nothing outside `src/agent` imports deeper; lint and the architecture test reject it. + +```ts +import { runAgent, RunOutcome } from '@agent'; +import type { RunConfig, RunInput, RunResult, AgentProgress } from '@agent/types'; + +runAgent(config: RunConfig, input: RunInput, options?: { + onProgress?: (event: AgentProgress) => void; + interaction?: AgentInteraction; +}): Promise +``` + +- `RunConfig`: the program id, its `AgentRunDefinition` (prompt, skill, tools, copy), the resolved `binding` (sequence, harness, model), the switchboard inputs, the skills origin, flag snapshot, trace tags, tool allow and deny lists, seed tasks and bound completion `hooks`. +- `RunInput`: install directory, resolved credentials, project and user payloads, skill id, detected integration, `flags` (`ci`, `signup`, `debug`, `e2eAsk`, `localMcp`, `captureAio`, `benchmark`, `yaraReport`) and the host the CLI was told. +- `RunResult`: `outcome` is `RunOutcome.Success | Aborted | Failed | Crashed`. Success may carry an `outro`; the other three carry a `failure` (`AgentFailure`: message, outro data, error, exit code, error code, detail). Every result carries `skillId` and a `snapshot` of what the run reported: tasks, status lines, stage, token usage totals, final cost, dashboard and notebook URLs, handoff text. +- `AgentProgress`: one event per thing the run reports, in emission order. Kinds: `lifecycle`, `spinner`, `log`, `status`, `tasks`, `stage`, `url`, `usage`, `finalCost`, `authError`, `handoff`, `completion`. Payloads are copies, never live objects. +- `AgentInteraction`: every member optional. `ask(question)` resolves with answers, `cancelAsk()` dismisses the open question, `taskNotice(notice)` resolves with whether to keep an optional task, `cancelTaskNotice()` declines it. +- Errors: the agent does not exit the process and does not throw for a decided failure. An unexpected throw becomes `outcome: Crashed` with the error attached. A gateway 401 emits `authError` and then fails. + +Other runtime exports: `resolveBinding`, `shouldDisableAsk`, `initializeAgent`, `executeAgent`, `buildRunTags`, `AgentSignals`, `configureGatewayFromCIEnvironment`, `downloadSkill`, `WIZARD_TOOL_NAMES`, `LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and `runMcpPromptViaSdk`, which loads the streaming module on first call. + +Minimal invocation: + +```ts +const result = await runAgent(config, input, { + onProgress: (event) => { + if (event.kind === 'log') console.log(event.message); + }, + interaction: { + ask: async (question) => answersFor(question), + }, +}); +if (result.outcome !== RunOutcome.Success) { + process.exitCode = result.failure.exitCode ?? 1; +} +``` + +`src/agent/__tests__/run-agent-standalone.test.ts` runs this with no UI, no store and no registry. + +## Intent + +Programs call the agent to do the work a skill describes. The TUI and the headless runner observe the run through `onProgress` and answer it through `interaction`; today `src/lib/programs/run-agent-legacy.ts` does both on top of the session. + +Without `onProgress` the run completes and its snapshot still comes back in the result. Without `interaction` the agent installs no ask bridge: `wizard_ask` returns its "not available" error and optional task notices are declined, which is what a `--ci` run does. A throwing observer is logged and the run continues. + +## Architecture + +The agent owns run state for one invocation: the task queue, phase, status, resolved skill, handoff text, usage and the final result. It depends on `src/shared` and on `src/env.ts`, and on program types only until the bindings table moves to programs. + +```text +caller ── RunConfig + RunInput ──▶ runAgent + │ prepareRun: gateway mint, triage provider + ▼ + sequence (linear | orchestrator) + │ + harness (anthropic | pi) ── tools (MCP or pi-native) + │ + onProgress ◀── events ───┤──── questions ──▶ interaction + ▼ + RunResult +``` + +`runner/` holds the dispatcher, sequences, harnesses and the switchboard. `tools/` holds the wizard tools shared by both harnesses. `middleware/` holds the benchmark pipeline. `progress.ts` defines the event and interaction contracts; `yara-hooks.ts` scans what the run installs. diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index 5f43656cb..79a87bfad 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -32,7 +32,7 @@ New models require Wizard capabilities **and** mint model/effort allowlists, gateway provider/transport support, and compatibility with required Wizard and security-triage prompt policies. Local model constants cannot bypass admission. See the -[development guide](../../../../.claude/skills/wizard-development/SKILL.md#execution-policy-and-model-admission) +[development guide](../../../.claude/skills/wizard-development/SKILL.md#execution-policy-and-model-admission) for the coordinated change checklist. ## The pieces diff --git a/src/agent/runner/harness/anthropic/README.md b/src/agent/runner/harness/anthropic/README.md index 87b39992c..d52a8d3d2 100644 --- a/src/agent/runner/harness/anthropic/README.md +++ b/src/agent/runner/harness/anthropic/README.md @@ -12,7 +12,7 @@ supported: `run()` for linear conversations and `runTask()` for orchestrator seed/task calls. Pi also implements both entry points. The SDK subprocess uses the scoped token minted by -[gateway-session.ts](../../../../gateway-session.ts). Wizard explicitly sets the +[gateway-session.ts](../../../gateway-session.ts). Wizard explicitly sets the gateway URL and authentication environment and isolates stored Claude logins. Model selection must satisfy local routing, the SDK's supported transport, mint model/effort allowlists, and the gateway's required prompt policy. The SDK is @@ -22,6 +22,6 @@ Security is enforced through `wizardCanUseTool`, SDK sandbox configuration, and warlock pre/post tool hooks. Sensitive question answers use vault references; write operations are also guarded while a question overlay is open. Read [agent-interface.ts](../../../agent-interface.ts), -[yara-hooks.ts](../../../../yara-hooks.ts), and -[wizard-tools](../../../../wizard-tools/) for current tool registration and +[yara-hooks.ts](../../../yara-hooks.ts), and +[wizard-tools](../../../tools/) for current tool registration and permission behavior rather than maintaining a second tool inventory here. diff --git a/src/agent/runner/sequence/README.md b/src/agent/runner/sequence/README.md index d50ce96ad..ac4436f9a 100644 --- a/src/agent/runner/sequence/README.md +++ b/src/agent/runner/sequence/README.md @@ -29,7 +29,7 @@ prompt and valid task-skill variants. Metrics already has a Pi/orchestrator binding; other routes can be selected through scoped flags or development CLI overrides. The retired `experiment/orchestrator` branch is not a prerequisite. See -[adding a program](../../../../../.claude/skills/adding-skill-program/SKILL.md). +[adding a program](../../../../.claude/skills/adding-skill-program/SKILL.md). ### Task metadata diff --git a/src/shared/README.md b/src/shared/README.md new file mode 100644 index 000000000..ae2a39c25 --- /dev/null +++ b/src/shared/README.md @@ -0,0 +1,43 @@ +# Shared + +Stateless library code every surface may import: constants, the API and host types, error codes, fetch retry, the skill menu, Claude settings handling, the secret vault, health checks and the utilities under `utils/`. Nothing here depends on the agent, the programs, the TUI or the CLI. + +## Signatures + +Import by path: `@shared/` for the singles and `@utils/` for `src/shared/utils`. There is no barrel; a shared library loads what a caller names and nothing else. + +Modules callers reach most: + +- `@shared/constants`: integrations, harnesses, sequences, URLs and `getSkillsBaseUrl()`. +- `@shared/errors`: `ErrorCodes`, `WizardError`, `emitWizardError`, `classifyRunFailure`, `classifyAuthFailure`, the catalog. +- `@shared/api`: `Credentials`, `ApiUser`, `ApiProject`. +- `@shared/host-resolution`: `HostResolution`, the immutable snapshot of where the wizard talks to. +- `@shared/fetch-retry`: `fetchWithRetry(url, { fetchImpl?, sleepImpl?, maxAttempts? })`, one retry and failover policy for every critical-path fetch. +- `@shared/skill-menu`: `fetchSkillMenu(skillsBaseUrl, retryOpts?)` returns the parsed `SkillMenu` or `null`; `expandBundleEntry`, `SkillEntry`, `CliEntry`. +- `@shared/claude-settings`: settings conflict detection, backup and restore. +- `@shared/secret-vault`: the session-scoped vault the tools resolve secret references through. +- `@shared/health-checks`: `evaluateWizardReadiness`, `checkAllExternalServices` and the gateway and skills-origin endpoint checks. +- `@utils/debug`: `logToFile`, `debug`, `enableDebugLogs`, `setDebugSink`. +- `@utils/analytics`: the `analytics` client (`wizardCapture`, `captureException`, `setTag`, `flush`, `shutdown`). +- `@utils/telemetry`: `withProgress(step, fn)` tags analytics with the current step and runs `fn`. +- `@utils/package-manager`, `@utils/env-scan`, `@utils/bounded-fs`, `@utils/atomic-ledger`, `@utils/semver`, `@utils/urls`, `@utils/links`. + +Callback convention: a shared helper that needs to show something takes a sink or returns data. It never looks the UI up. + +```ts +import { debug, setDebugSink } from '@utils/debug'; + +const restore = setDebugSink((line) => myLog.info(line)); +debug('resolving host', host); // rendered and handed to the sink +setDebugSink(restore); +``` + +`src/ui/index.ts` installs the current UI's info log as the debug sink at load, so `debug()` follows `setUI()` without shared code knowing a UI exists. Until the UI module loads, lines go to stdout. + +## Intent + +Shared exists so the agent, programs, TUI, headless and CLI code can use one implementation of the things they all need without importing each other. A helper belongs here when it holds no run state, needs no surface-specific dependency, and would otherwise be copied. + +## Architecture + +Shared imports `src/env.ts` and itself. The architecture test classifies `src/shared` as its own surface and lists the remaining upward edges in `src/__tests__/architecture/known-violations.json`; each has an owner in the stack plan. `utils/setup-utils.ts`, `utils/oauth.ts` and `utils/wizard-abort.ts` are TUI and CLI flow code that leave in Release C; `utils/analytics.ts` reads the session until Release B; `claude-settings.ts` and `errors/agent-map.ts` import two agent leaf modules until Release B, because the agent entry would form a module cycle through analytics. From 67ece688749ab1d10725ae64b2b464626ed1da4b Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 15:07:27 -0400 Subject: [PATCH 29/61] docs(agent): name what stays in the entry modules and what leaves Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/gateway-session.ts | 3 +++ src/agent/index.ts | 30 ++++++++++++++++++++++++++++ src/agent/runner/shared/bootstrap.ts | 5 +++++ src/agent/types.ts | 8 ++++++++ 4 files changed, 46 insertions(+) diff --git a/src/agent/gateway-session.ts b/src/agent/gateway-session.ts index 9932f36ad..e8a1ef1b2 100644 --- a/src/agent/gateway-session.ts +++ b/src/agent/gateway-session.ts @@ -72,6 +72,9 @@ export function configureGatewayCredentialsForCI( }; } +// TODO(B2): CI credential loading belongs to the headless provider, not the +// agent. Leaves with the rest of this module once RunInput carries resolved +// inference auth. export function configureGatewayFromCIEnvironment( projectId: number, region: CloudRegion, diff --git a/src/agent/index.ts b/src/agent/index.ts index c014021cc..42ca8a005 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -4,6 +4,35 @@ * architecture test. Keep this list to what callers use, and keep it cheap: * the startup chunk imports this module, so anything re-exported here loads * before the wizard does any work. Heavy paths stay behind a lazy import. + * + * Audited against the stack plan (docs: workbench/wizard-functional-stack-plan.md, + * sections 4.1 to 4.5 and 7). What stays and what leaves: + * + * Final, the agent's contract: + * runAgent, RunOutcome the one way to run the agent (4.1) + * AgentSignals prompt marker strings program prompts embed + * WIZARD_TOOL_NAMES tool ids programs put in allowedTools/disallowedTools + * + * Leaves in B1 (bindings and program data move to programs, 4.5): + * resolveBinding keyed by PROGRAM_BINDINGS; agent keeps only + * "run from an already-resolved binding" + * shouldDisableAsk a flags policy programs decide, then pass in + * LONGER_ASK_TIMEOUT_MS a tuning number; programs own askTimeoutMs + * + * Leaves in B2 (programs own credentials and the legacy adapter dies): + * initializeAgent, executeAgent, buildRunTags + * the pre-runAgent surface detection/agentic.ts + * and run-agent-legacy.ts still call; they go + * through runAgent or leave with detection + * configureGatewayFromCIEnvironment + * CI inference auth; headless provider owns it + * flushScanReport the report becomes a progress event, not a call + * + * Leaves in A4 (scan at load; skill install becomes shared): + * downloadSkill + * + * Leaves in C2 (the TUI receives agent data through program state): + * runMcpPromptViaSdk */ export type * from './types'; export { @@ -18,6 +47,7 @@ export { initializeAgent, runAgent as executeAgent, } from './agent-interface'; +// TODO(B2): leaves the entry when the headless provider owns CI gateway auth. export { configureGatewayFromCIEnvironment } from './gateway-session'; export { downloadSkill, WIZARD_TOOL_NAMES } from './tools'; export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; diff --git a/src/agent/runner/shared/bootstrap.ts b/src/agent/runner/shared/bootstrap.ts index 43d8a570f..1ed76bae0 100644 --- a/src/agent/runner/shared/bootstrap.ts +++ b/src/agent/runner/shared/bootstrap.ts @@ -84,6 +84,11 @@ export async function prepareRun( // readers re-resolve through the cache, which re-mints past the refresh // point. const currentGatewayAuth = () => + // TODO(B2): the agent must not mint inference auth. It receives the + // PostHog token here and derives a gateway token from it, re-minting near + // expiry. Programs own credentials (stack plan 4.5): pass a resolved + // inference-auth provider on RunInput.credentials and move + // gateway-session.ts out of src/agent with it. gatewayAuth(credentials.host, credentials.accessToken, programId); await currentGatewayAuth(); diff --git a/src/agent/types.ts b/src/agent/types.ts index 6b42c298a..549a41b09 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -2,6 +2,14 @@ * Public type surface of the agent. Type-only, so importing it adds no * runtime dependency. Code outside `src/agent` imports these as * `@agent/types`; runtime values come from `@agent`. + * + * Audited against the stack plan (sections 4.1 to 4.3): the run contract + * (RunConfig, RunInput, RunResult, AgentRunDefinition, AgentFailure, + * AbortCase, PromptContext) and the progress and interaction contracts + * (AgentProgress, AgentInteraction and their payload shapes) are final. + * ProgramBinding and SwitchboardCtx leave in B1 with the bindings table. + * AgentChunk leaves in C2 with runMcpPromptViaSdk. InstallSkillResult + * leaves in A4 with downloadSkill. */ export type { AbortCase, From 3e2f1be2ce042a2be5a5321d2ad4fead64947070 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 15:09:07 -0400 Subject: [PATCH 30/61] docs(agent): skill install leaves with Release B, not a separate stage Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/index.ts | 3 ++- src/agent/types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/agent/index.ts b/src/agent/index.ts index 42ca8a005..f92999dee 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -28,7 +28,8 @@ * CI inference auth; headless provider owns it * flushScanReport the report becomes a progress event, not a call * - * Leaves in A4 (scan at load; skill install becomes shared): + * Leaves with Release B, when the skill scan moves to load time and skill + * install becomes shared: * downloadSkill * * Leaves in C2 (the TUI receives agent data through program state): diff --git a/src/agent/types.ts b/src/agent/types.ts index 549a41b09..0f9bad690 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -9,7 +9,7 @@ * (AgentProgress, AgentInteraction and their payload shapes) are final. * ProgramBinding and SwitchboardCtx leave in B1 with the bindings table. * AgentChunk leaves in C2 with runMcpPromptViaSdk. InstallSkillResult - * leaves in A4 with downloadSkill. + * leaves with downloadSkill in Release B. */ export type { AbortCase, From c620d09fe15fe9e7ff9383efefcfa0f86f9dd952 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 15:11:22 -0400 Subject: [PATCH 31/61] docs(agent): group the entry exports by when they leave Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/index.ts | 76 +++++++++++++++++++++------------------------- src/agent/types.ts | 24 +++++++-------- 2 files changed, 46 insertions(+), 54 deletions(-) diff --git a/src/agent/index.ts b/src/agent/index.ts index f92999dee..0c2c6d22b 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -5,59 +5,51 @@ * the startup chunk imports this module, so anything re-exported here loads * before the wizard does any work. Heavy paths stay behind a lazy import. * - * Audited against the stack plan (docs: workbench/wizard-functional-stack-plan.md, - * sections 4.1 to 4.5 and 7). What stays and what leaves: - * - * Final, the agent's contract: - * runAgent, RunOutcome the one way to run the agent (4.1) - * AgentSignals prompt marker strings program prompts embed - * WIZARD_TOOL_NAMES tool ids programs put in allowedTools/disallowedTools - * - * Leaves in B1 (bindings and program data move to programs, 4.5): - * resolveBinding keyed by PROGRAM_BINDINGS; agent keeps only - * "run from an already-resolved binding" - * shouldDisableAsk a flags policy programs decide, then pass in - * LONGER_ASK_TIMEOUT_MS a tuning number; programs own askTimeoutMs - * - * Leaves in B2 (programs own credentials and the legacy adapter dies): - * initializeAgent, executeAgent, buildRunTags - * the pre-runAgent surface detection/agentic.ts - * and run-agent-legacy.ts still call; they go - * through runAgent or leave with detection - * configureGatewayFromCIEnvironment - * CI inference auth; headless provider owns it - * flushScanReport the report becomes a progress event, not a call - * - * Leaves with Release B, when the skill scan moves to load time and skill - * install becomes shared: - * downloadSkill - * - * Leaves in C2 (the TUI receives agent data through program state): - * runMcpPromptViaSdk + * Grouped by fate, per the stack plan (sections 4.1 to 4.5 and 7). + */ + +/** + * Stays. The agent's contract: the one way to run it, the marker strings + * program prompts embed, and the tool ids programs put in allowedTools and + * disallowedTools. */ export type * from './types'; +export { runAgent, RunOutcome } from './runner'; +export { AgentSignals } from './agent-interface'; +export { WIZARD_TOOL_NAMES } from './tools'; + +/** + * Leaves in B1. Bindings and program data move to programs: resolveBinding + * is keyed by PROGRAM_BINDINGS and the agent keeps only "run from an + * already-resolved binding"; shouldDisableAsk is a flags policy programs + * decide and pass in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own + * as askTimeoutMs. + */ +export { resolveBinding, shouldDisableAsk } from './runner'; +export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; + +/** + * Leaves in B2. Programs own credentials and the legacy adapter dies. + * initializeAgent, executeAgent and buildRunTags are the pre-runAgent surface + * that detection/agentic.ts and run-agent-legacy.ts still call; they go + * through runAgent or leave with detection. configureGatewayFromCIEnvironment + * is CI inference auth the headless provider owns. flushScanReport becomes a + * progress event rather than a call. downloadSkill leaves once the skill scan + * runs at load and skill install becomes shared. + */ export { - resolveBinding, - runAgent, - RunOutcome, - shouldDisableAsk, -} from './runner'; -export { - AgentSignals, buildRunTags, initializeAgent, runAgent as executeAgent, } from './agent-interface'; -// TODO(B2): leaves the entry when the headless provider owns CI gateway auth. export { configureGatewayFromCIEnvironment } from './gateway-session'; -export { downloadSkill, WIZARD_TOOL_NAMES } from './tools'; -export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; export { flushScanReport } from './yara-hooks'; +export { downloadSkill } from './tools'; /** - * Stream an MCP tutorial prompt through the SDK. Loaded on first call: the - * streaming module is the one agent module the startup chunk does not already - * hold, and the TUI reaches it only from the suggested-prompts screen. + * Leaves in C2. The TUI receives agent data through program state. Until + * then the suggested-prompts screen streams through this wrapper, which loads + * the streaming module on first call so the startup chunk does not grow. */ export async function* runMcpPromptViaSdk( args: Parameters< diff --git a/src/agent/types.ts b/src/agent/types.ts index 0f9bad690..d9a4b42a8 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -1,26 +1,19 @@ /** * Public type surface of the agent. Type-only, so importing it adds no * runtime dependency. Code outside `src/agent` imports these as - * `@agent/types`; runtime values come from `@agent`. - * - * Audited against the stack plan (sections 4.1 to 4.3): the run contract - * (RunConfig, RunInput, RunResult, AgentRunDefinition, AgentFailure, - * AbortCase, PromptContext) and the progress and interaction contracts - * (AgentProgress, AgentInteraction and their payload shapes) are final. - * ProgramBinding and SwitchboardCtx leave in B1 with the bindings table. - * AgentChunk leaves in C2 with runMcpPromptViaSdk. InstallSkillResult - * leaves with downloadSkill in Release B. + * `@agent/types`; runtime values come from `@agent`. Grouped by fate, per the + * stack plan (sections 4.1 to 4.3). */ + +/** Stays. The run contract and the progress and interaction contracts. */ export type { AbortCase, AgentFailure, AgentRunDefinition, - ProgramBinding, PromptContext, RunConfig, RunInput, RunResult, - SwitchboardCtx, } from './runner'; export type { AgentInteraction, @@ -34,5 +27,12 @@ export type { TaskNotice, TokenUsageDelta, } from './progress'; -export type { AgentChunk } from './mcp-prompt-streaming'; + +/** Leaves in B1 with the bindings table. */ +export type { ProgramBinding, SwitchboardCtx } from './runner'; + +/** Leaves in B2 with downloadSkill. */ export type { InstallSkillResult } from './tools'; + +/** Leaves in C2 with runMcpPromptViaSdk. */ +export type { AgentChunk } from './mcp-prompt-streaming'; From 889af716e6f2a5f99d84162b93466f4a251dcd71 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 16:03:10 -0400 Subject: [PATCH 32/61] test(agent): cover public prompt streaming entry --- src/agent/__tests__/entry-streaming.test.ts | 108 ++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/agent/__tests__/entry-streaming.test.ts diff --git a/src/agent/__tests__/entry-streaming.test.ts b/src/agent/__tests__/entry-streaming.test.ts new file mode 100644 index 000000000..9ff311d90 --- /dev/null +++ b/src/agent/__tests__/entry-streaming.test.ts @@ -0,0 +1,108 @@ +import { rmSync } from 'node:fs'; +import { runMcpPromptViaSdk } from '@agent'; +import type { AgentChunk } from '@agent/types'; +import { + configureGatewayCredentialsForCI, + resetGatewaySession, +} from '@agent/gateway-session'; +import { HostResolution } from '@shared/host-resolution'; + +const { query } = vi.hoisted(() => ({ + query: + vi.fn< + (args: { + options: { env: { CLAUDE_CONFIG_DIR: string } }; + }) => Generator + >(), +})); + +vi.mock('@anthropic-ai/claude-agent-sdk', () => ({ query })); + +async function consume( + overrides: Partial[0]> = {}, +): Promise { + const chunks: AgentChunk[] = []; + for await (const chunk of runMcpPromptViaSdk({ + prompt: 'List events', + credentials: { + accessToken: 'test-access-token', + projectApiKey: 'test-project-key', + projectId: 1, + host: HostResolution.fromRegion('us'), + }, + signal: new AbortController().signal, + programId: 'mcp-tutorial', + ...overrides, + })) { + chunks.push(chunk); + } + return chunks; +} + +describe('public agent prompt stream', () => { + beforeEach(() => { + for (const name of [ + 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_AUTH_TOKEN', + 'CLAUDE_CODE_OAUTH_TOKEN', + 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS', + ]) { + vi.stubEnv(name, process.env[name]); + } + configureGatewayCredentialsForCI( + 'test-gateway-token', + 1, + 'https://ai-gateway.us.posthog.com', + ); + }); + + afterEach(() => { + for (const [args] of query.mock.calls) { + rmSync(args.options.env.CLAUDE_CONFIG_DIR, { + recursive: true, + force: true, + }); + } + query.mockReset(); + resetGatewaySession(); + vi.unstubAllEnvs(); + }); + + it('forwards text and completion with the resumable session ID', async () => { + query.mockImplementation(function* () { + yield { + type: 'assistant', + message: { content: [{ type: 'text', text: 'Found three events.' }] }, + }; + yield { type: 'result', subtype: 'success', session_id: 'session-123' }; + }); + + await expect(consume()).resolves.toEqual([ + { kind: 'text', text: 'Found three events.' }, + { kind: 'done', sessionId: 'session-123' }, + ]); + }); + + it('forwards an SDK stream failure after partial output', async () => { + query.mockImplementation(function* () { + yield { + type: 'assistant', + message: { content: [{ type: 'text', text: 'Checking events...' }] }, + }; + throw new Error('SDK connection lost'); + }); + + await expect(consume()).resolves.toEqual([ + { kind: 'text', text: 'Checking events...' }, + { kind: 'error', text: 'SDK connection lost' }, + ]); + }); + + it('propagates setup failures instead of silently ending the stream', async () => { + resetGatewaySession(); + + await expect(consume({ programId: undefined })).rejects.toThrow( + 'this run has no program to attribute its spend to', + ); + }); +}); From 3ae7846a35afb0f171402d490eff4b2392cf1785 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 16:16:51 -0400 Subject: [PATCH 33/61] refactor(programs): move program surface and TUI decks Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../skills/adding-framework-support/SKILL.md | 24 +-- .claude/skills/adding-skill-program/SKILL.md | 28 +-- .claude/skills/exploring-the-wizard/SKILL.md | 2 +- .claude/skills/ink-tui/SKILL.md | 2 +- .../skills/ink-tui/references/ARCHITECTURE.md | 4 +- .claude/skills/wizard-development/SKILL.md | 6 +- .../references/ARCHITECTURE.md | 2 +- .eslintrc.cjs | 9 +- AGENTS.md | 10 +- README.md | 22 ++- docs/error-catalog.md | 4 +- docs/runbooks/agentic-monorepo-detection.md | 6 +- .../__tests__/e2e-flow-snapshot.test.ts | 6 +- e2e-harness/__tests__/e2e-profile-ask.test.ts | 2 +- e2e-harness/__tests__/e2e-result.test.ts | 2 +- .../__tests__/wizard-ci-driver.test.ts | 6 +- e2e-harness/action-registry.ts | 4 +- e2e-harness/e2e-result.ts | 4 +- e2e-harness/profiles.ts | 22 +-- package.json | 1 + scripts/tui-host.no-jest.ts | 20 +- .../architecture/import-boundaries.test.ts | 86 +++++--- .../architecture/known-violations.json | 185 +++++++++++++++--- src/__tests__/cli.test.ts | 10 +- src/__tests__/mcp-cli.test.ts | 2 +- src/__tests__/programs-cli.test.ts | 6 +- src/__tests__/provision-cli.test.ts | 8 +- src/agent/README.md | 2 +- .../credential-isolation-snapshot.test.ts | 2 +- .../__tests__/run-agent-standalone.test.ts | 3 +- src/agent/__tests__/wizard-tools.test.ts | 2 +- src/agent/agent-interface.ts | 2 +- src/agent/agent-runner.ts | 2 +- .../agent-map.ts => agent/error-map.ts} | 7 +- src/agent/mcp-prompt-streaming.ts | 2 +- src/agent/progress.ts | 2 +- src/agent/runner/README.md | 2 +- .../runner/__tests__/switchboard.test.ts | 2 +- src/agent/runner/index.ts | 2 +- src/agent/runner/sequence/linear.ts | 3 +- src/agent/runner/shared/types.ts | 4 +- .../switchboard/flags/__tests__/flags.test.ts | 2 +- src/agent/runner/switchboard/flags/index.ts | 2 +- src/agent/runner/switchboard/flags/schemes.ts | 2 +- src/agent/runner/switchboard/index.ts | 2 +- src/commands/ai-observability.ts | 2 +- src/commands/audit.ts | 2 +- src/commands/basic-integration/ci-install.ts | 2 +- src/commands/basic-integration/interactive.ts | 2 +- src/commands/basic-integration/skill.ts | 2 +- src/commands/doctor.ts | 2 +- src/commands/error-tracking.ts | 2 +- .../__tests__/native-command-factory.test.ts | 2 +- .../factories/family-command-factory.ts | 4 +- .../factories/native-command-factory.ts | 2 +- src/commands/factories/shared.ts | 2 +- src/commands/mcp-analytics.ts | 2 +- src/commands/mcp/add.ts | 2 +- src/commands/mcp/remove.ts | 2 +- src/commands/mcp/tutorial.ts | 2 +- src/commands/metrics.ts | 2 +- src/commands/migrate.ts | 2 +- src/commands/replay-vision.ts | 2 +- src/commands/revenue.ts | 2 +- src/commands/self-driving.ts | 2 +- src/commands/slack.ts | 2 +- src/commands/upload-sourcemaps.ts | 2 +- src/commands/warehouse.ts | 2 +- src/lib/__tests__/wizard-spellbook.test.ts | 2 +- src/lib/registry.ts | 59 ------ .../runners/__tests__/mint-recovery.test.ts | 10 +- src/lib/runners/run-non-interactive.ts | 14 +- src/lib/runners/run-wizard-ci.ts | 2 +- src/lib/runners/run-wizard-headless.ts | 2 +- src/lib/runners/run-wizard.ts | 20 +- src/lib/wizard-session.ts | 2 +- src/lib/wizard-spellbook.ts | 2 +- .../__snapshots__/flow-traces.test.ts.snap | 0 .../post-auth-gates.test.ts.snap | 0 .../programs/__tests__/agent-skill.test.ts | 4 +- .../programs/__tests__/audit-seed.test.ts | 4 +- .../__tests__/cloudflare-detection.test.ts | 2 +- .../programs/__tests__/detect-map.test.ts | 0 .../programs/__tests__/error-tracking.test.ts | 28 +-- .../programs/__tests__/flow-traces.test.ts | 2 +- .../programs/__tests__/mcp-analytics.test.ts | 2 +- .../__tests__/metrics-program.test.ts | 10 +- .../__tests__/post-auth-gates.test.ts | 0 .../__tests__/posthog-cli-preinstall.test.ts | 2 +- .../posthog-integration-detect.test.ts | 2 +- .../__tests__/program-registry.test.ts | 2 +- .../programs/__tests__/program-step.test.ts | 2 +- .../refresh-access-token-if-needed.test.ts | 0 .../programs/__tests__/replay-vision.test.ts | 2 +- .../revenue-analytics-detect.test.ts | 2 +- .../__tests__/run-agent-legacy.test.ts | 6 +- .../__tests__/self-driving-deck.test.ts | 2 +- .../__tests__/self-driving-detect.test.ts | 14 +- .../__tests__/self-driving-prompt.test.ts | 4 +- .../source-maps-detect-agentic.test.ts | 4 +- .../__tests__/source-maps-detect.test.ts | 2 +- .../__tests__/source-maps-prompt.test.ts | 2 +- .../__tests__/warehouse-ask-timeout.test.ts | 2 +- .../__tests__/warehouse-seed-task.test.ts | 6 +- .../__tests__/warehouse-suggestion.test.ts | 8 +- .../web-analytics-doctor-detect.test.ts | 2 +- src/{lib => }/programs/agent-skill/index.ts | 6 +- src/{lib => }/programs/agent-skill/steps.ts | 4 +- .../programs/ai-observability/index.ts | 6 +- .../programs/ai-observability/test/e2e.json | 0 src/{lib => }/programs/ai-opt-in-gate.ts | 0 src/{lib => }/programs/audit/detect.ts | 0 src/{lib => }/programs/audit/index.ts | 6 +- .../programs/audit/ledger-watcher.ts | 0 src/{lib => }/programs/audit/seed.ts | 0 src/{lib => }/programs/audit/test/e2e.json | 0 src/{lib => }/programs/audit/types.ts | 0 src/{lib => }/programs/authenticate.ts | 2 +- src/{lib => programs}/cloudflare-detection.ts | 0 src/{lib => }/programs/detect-map.ts | 10 +- .../__tests__/agentic-progress.test.ts | 0 .../detection/__tests__/agentic.test.ts | 2 +- .../detection/__tests__/context.test.ts | 4 +- .../detection/__tests__/features.test.ts | 2 +- .../detection/__tests__/framework.test.ts | 16 +- .../__tests__/package-manager.test.ts | 2 +- .../detection/__tests__/project-scope.test.ts | 12 +- src/{lib => programs}/detection/agentic.ts | 2 +- src/{lib => programs}/detection/context.ts | 2 +- src/{lib => programs}/detection/features.ts | 0 src/{lib => programs}/detection/framework.ts | 2 +- src/{lib => programs}/detection/index.ts | 0 .../detection/package-manager.ts | 2 +- .../detection/project-scope.ts | 4 +- src/{lib => }/programs/dispatch-family.ts | 14 +- .../detect-agentic.ts | 4 +- .../detect.ts | 0 .../index.ts | 8 +- .../prompt.ts | 0 .../steps.ts | 2 +- .../test/e2e.json | 0 .../programs/error-tracking/detect-agentic.ts | 6 +- .../programs/error-tracking/index.ts | 20 +- .../programs/error-tracking/test/e2e.json | 0 .../programs/events-audit/constants.ts | 0 src/{lib => }/programs/events-audit/index.ts | 12 +- src/{lib => }/programs/events-audit/seed.ts | 2 +- src/{lib => }/programs/events-audit/steps.ts | 4 +- src/{lib => programs}/framework-config.ts | 0 .../android/android-wizard-agent.ts | 4 +- .../frameworks/android/utils.ts | 0 .../angular/angular-wizard-agent.ts | 4 +- .../frameworks/angular/utils.ts | 0 .../frameworks/astro/astro-wizard-agent.ts | 4 +- src/{ => programs}/frameworks/astro/utils.ts | 0 .../frameworks/django/django-wizard-agent.ts | 6 +- src/{ => programs}/frameworks/django/utils.ts | 0 .../frameworks/elixir/elixir-wizard-agent.ts | 4 +- .../fastapi/fastapi-wizard-agent.ts | 6 +- .../frameworks/fastapi/utils.ts | 0 .../frameworks/flask/flask-wizard-agent.ts | 6 +- src/{ => programs}/frameworks/flask/utils.ts | 0 .../flutter/flutter-wizard-agent.ts | 4 +- .../frameworks/go/go-wizard-agent.ts | 4 +- .../frameworks/java/java-wizard-agent.ts | 4 +- .../javascript-node-wizard-agent.ts | 4 +- .../javascript-web-wizard-agent.ts | 4 +- .../frameworks/javascript-web/utils.ts | 0 .../frameworks/kmp/kmp-wizard-agent.ts | 4 +- .../laravel/laravel-wizard-agent.ts | 4 +- .../frameworks/laravel/utils.ts | 0 .../frameworks/nextjs/nextjs-wizard-agent.ts | 4 +- src/{ => programs}/frameworks/nextjs/utils.ts | 0 .../frameworks/nuxt/nuxt-wizard-agent.ts | 4 +- .../frameworks/python/python-wizard-agent.ts | 6 +- src/{ => programs}/frameworks/python/utils.ts | 0 .../frameworks/rails/rails-wizard-agent.ts | 4 +- src/{ => programs}/frameworks/rails/utils.ts | 0 .../react-native/react-native-wizard-agent.ts | 4 +- .../frameworks/react-native/utils.ts | 0 .../react-router/react-router-wizard-agent.ts | 4 +- .../frameworks/react-router/utils.ts | 0 .../frameworks/ruby/ruby-wizard-agent.ts | 4 +- src/{ => programs}/frameworks/ruby/utils.ts | 0 .../frameworks/rust/rust-wizard-agent.ts | 4 +- .../frameworks/svelte/svelte-wizard-agent.ts | 4 +- .../frameworks/swift/__tests__/utils.test.ts | 0 .../frameworks/swift/swift-wizard-agent.ts | 4 +- src/{ => programs}/frameworks/swift/utils.ts | 0 .../tanstack-router-wizard-agent.ts | 4 +- .../frameworks/tanstack-router/utils.ts | 0 .../tanstack-start-wizard-agent.ts | 4 +- .../frameworks/tanstack-start/utils.ts | 0 .../frameworks/vue/vue-wizard-agent.ts | 4 +- src/programs/index.ts | 10 + src/{lib => }/programs/mcp-analytics/index.ts | 2 +- src/{lib => }/programs/mcp/index.ts | 2 +- src/{lib => }/programs/metrics/index.ts | 6 +- src/{lib => }/programs/metrics/test/e2e.json | 0 src/{lib => }/programs/migration/index.ts | 4 +- src/{lib => }/programs/migration/steps.ts | 4 +- .../oauth/__tests__/program-scopes.test.ts | 2 +- src/{lib => programs}/oauth/program-scopes.ts | 2 +- .../__tests__/doctor-schema.test.ts | 4 +- .../programs/posthog-doctor/fetch.ts | 0 .../programs/posthog-doctor/index.ts | 2 +- .../programs/posthog-doctor/kind-metadata.ts | 0 .../programs/posthog-doctor/steps.ts | 4 +- .../programs/posthog-doctor/types.ts | 0 .../__tests__/detect.test.ts | 18 +- .../__tests__/handoff.test.ts | 0 .../__tests__/index.test.ts | 2 +- .../programs/posthog-integration/constants.ts | 0 .../programs/posthog-integration/detect.ts | 16 +- .../programs/posthog-integration/handoff.ts | 0 .../programs/posthog-integration/index.ts | 21 +- .../programs/posthog-integration/steps.ts | 4 +- .../posthog-integration/test/README.md | 2 +- .../posthog-integration/test/e2e.json | 0 src/{lib => }/programs/program-registry.ts | 4 +- src/{lib => }/programs/program-run.ts | 0 src/{lib => }/programs/program-step.ts | 4 +- src/programs/registry.ts | 59 ++++++ src/{lib => }/programs/replay-vision/index.ts | 17 +- .../programs/replay-vision/test/e2e.json | 0 .../programs/revenue-analytics/detect.ts | 4 +- .../programs/revenue-analytics/index.ts | 4 +- .../programs/revenue-analytics/steps.ts | 4 +- src/{lib => }/programs/run-agent-legacy.ts | 2 +- .../programs/self-driving/ARCHITECTURE.md | 24 +-- .../self-driving/__tests__/step-keys.test.ts | 0 .../programs/self-driving/detect-agentic.ts | 8 +- src/{lib => }/programs/self-driving/detect.ts | 6 +- src/{lib => }/programs/self-driving/index.ts | 10 +- src/{lib => }/programs/self-driving/prompt.ts | 2 +- .../programs/self-driving/step-keys.ts | 0 src/{lib => }/programs/self-driving/steps.ts | 8 +- .../programs/self-driving/test/e2e.json | 0 .../programs/shared/health-check-step.ts | 2 +- .../programs/shared/package-scanning.ts | 0 .../programs/shared/posthog-cli-preinstall.ts | 0 src/{lib => }/programs/slack/index.ts | 2 +- .../task-stream/__tests__/audit-areas.test.ts | 6 +- .../__tests__/event-plan-watcher.test.ts | 4 +- .../__tests__/file-destination.test.ts | 7 +- .../__tests__/posthog-destination.test.ts | 2 +- .../__tests__/task-stream-push.test.ts | 8 +- .../task-stream/audit-areas.ts | 2 +- .../task-stream/destinations/file.ts | 2 +- .../task-stream/destinations/posthog.ts | 2 +- .../task-stream/event-plan-watcher.ts | 0 src/{lib => programs}/task-stream/index.ts | 0 .../task-stream/task-stream-push.ts | 0 src/{lib => programs}/task-stream/types.ts | 0 src/programs/types.ts | 9 + .../programs/warehouse-source/detect.ts | 4 +- .../programs/warehouse-source/index.ts | 6 +- .../programs/warehouse-source/steps.ts | 2 +- .../programs/warehouse-source/test/e2e.json | 0 .../__tests__/detect.test.ts | 4 +- .../warehouse-sources/detect.ts | 2 +- .../warehouse-sources/registry.ts | 0 .../warehouse-sources/types.ts | 2 +- .../programs/web-analytics-doctor/detect.ts | 2 +- .../programs/web-analytics-doctor/index.ts | 2 +- .../programs/web-analytics-doctor/steps.ts | 4 +- .../__tests__/agent-env-isolation.test.ts | 2 +- src/{agent => shared}/agent-env-isolation.ts | 0 src/shared/claude-settings.ts | 5 +- src/shared/errors/index.ts | 1 - src/shared/utils/__tests__/oauth.test.ts | 2 +- src/shared/utils/env-scan.ts | 2 +- src/shared/utils/setup-utils.ts | 4 +- src/ui/tui/__tests__/frames.test.tsx | 20 +- .../__tests__/keyboard-equivalence.test.tsx | 6 +- src/ui/tui/__tests__/programs.test.ts | 2 +- src/ui/tui/__tests__/router.test.ts | 4 +- src/ui/tui/__tests__/store-invariants.test.ts | 4 +- src/ui/tui/__tests__/store.test.ts | 2 +- src/ui/tui/components/LearnCard.tsx | 4 +- .../tui/decks/agent-skill}/index.tsx | 0 .../index.tsx | 0 .../tui/decks/error-tracking}/index.tsx | 2 +- .../tui/decks/error-tracking}/tips.ts | 2 +- .../tui/decks/migration}/free-tier.tsx | 0 .../tui/decks/migration}/index.tsx | 6 +- .../decks/migration}/pricing-structure.tsx | 0 .../tui/decks/migration}/vendor-stack.tsx | 0 .../decks/posthog-integration}/data-flow.tsx | 0 .../tui/decks/posthog-integration}/funnel.tsx | 0 .../tui/decks/posthog-integration}/index.tsx | 0 .../decks/posthog-integration}/line-chart.tsx | 0 .../posthog-integration}/product-suite.tsx | 0 .../tui/decks/revenue-analytics}/index.tsx | 2 +- .../tui/decks/self-driving}/index.tsx | 0 .../decks/self-driving}/pipeline-diagram.tsx | 0 .../tui/decks/self-driving}/pricing.ts | 0 .../tui/decks/self-driving}/tips.ts | 0 .../tui/decks/warehouse-source}/index.tsx | 2 +- .../tui/playground/demos/AuditChecksDemo.tsx | 2 +- .../tui/playground/demos/DoctorReportDemo.tsx | 2 +- src/ui/tui/playground/demos/LearnDeckDemo.tsx | 2 +- src/ui/tui/playground/demos/RunScreenDemo.tsx | 4 +- src/ui/tui/router.ts | 2 +- src/ui/tui/screen-registry.tsx | 2 +- src/ui/tui/screen-sequences.ts | 9 +- .../tui/screens/ErrorTrackingDetectScreen.tsx | 4 +- .../tui/screens/McpSuggestedPromptsScreen.tsx | 2 +- .../screens/PostHogIntegrationIntroScreen.tsx | 4 +- src/ui/tui/screens/RevenueIntroScreen.tsx | 2 +- src/ui/tui/screens/RunScreen.tsx | 4 +- .../tui/screens/SelfDrivingGitHubScreen.tsx | 2 +- .../tui/screens/SelfDrivingHandoffScreen.tsx | 4 +- .../SelfDrivingIntegrationDetectScreen.tsx | 6 +- src/ui/tui/screens/SelfDrivingIntroScreen.tsx | 4 +- src/ui/tui/screens/SetupScreen.tsx | 2 +- src/ui/tui/screens/SlackConnectScreen.tsx | 2 +- src/ui/tui/screens/SourceMapsDetectScreen.tsx | 4 +- src/ui/tui/screens/WarehouseIntroScreen.tsx | 2 +- src/ui/tui/screens/audit/AuditAreaPane.tsx | 2 +- .../screens/audit/AuditChecksOutroSection.tsx | 2 +- .../AuditChecksViewer/AuditChecksViewer.tsx | 2 +- .../audit/AuditChecksViewer/CheckRow.tsx | 2 +- .../audit/AuditChecksViewer/DetailRow.tsx | 2 +- .../audit/AuditChecksViewer/Footer.tsx | 2 +- .../audit/AuditChecksViewer/Header.tsx | 2 +- .../screens/audit/AuditChecksViewer/sort.ts | 2 +- src/ui/tui/screens/audit/AuditOutroScreen.tsx | 2 +- src/ui/tui/screens/audit/AuditRunScreen.tsx | 4 +- .../tui/screens/audit/PendingChecksList.tsx | 2 +- .../tui/screens/doctor/DoctorReportScreen.tsx | 2 +- src/ui/tui/screens/doctor/IssueTable.tsx | 2 +- .../mcp-suggested-prompts-services.ts | 4 +- src/ui/tui/store.ts | 11 +- tsconfig.build.json | 6 +- vitest.config.ts | 14 +- 336 files changed, 907 insertions(+), 705 deletions(-) rename src/{shared/errors/agent-map.ts => agent/error-map.ts} (62%) delete mode 100644 src/lib/registry.ts rename src/{lib => }/programs/__tests__/__snapshots__/flow-traces.test.ts.snap (100%) rename src/{lib => }/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap (100%) rename src/{lib => }/programs/__tests__/agent-skill.test.ts (96%) rename src/{lib => }/programs/__tests__/audit-seed.test.ts (94%) rename src/{lib => programs}/__tests__/cloudflare-detection.test.ts (97%) rename src/{lib => }/programs/__tests__/detect-map.test.ts (100%) rename src/{lib => }/programs/__tests__/error-tracking.test.ts (88%) rename src/{lib => }/programs/__tests__/flow-traces.test.ts (99%) rename src/{lib => }/programs/__tests__/mcp-analytics.test.ts (97%) rename src/{lib => }/programs/__tests__/metrics-program.test.ts (86%) rename src/{lib => }/programs/__tests__/post-auth-gates.test.ts (100%) rename src/{lib => }/programs/__tests__/posthog-cli-preinstall.test.ts (97%) rename src/{lib => }/programs/__tests__/posthog-integration-detect.test.ts (96%) rename src/{lib => }/programs/__tests__/program-registry.test.ts (99%) rename src/{lib => }/programs/__tests__/program-step.test.ts (98%) rename src/{lib => }/programs/__tests__/refresh-access-token-if-needed.test.ts (100%) rename src/{lib => }/programs/__tests__/replay-vision.test.ts (97%) rename src/{lib => }/programs/__tests__/revenue-analytics-detect.test.ts (97%) rename src/{lib => }/programs/__tests__/run-agent-legacy.test.ts (98%) rename src/{lib => }/programs/__tests__/self-driving-deck.test.ts (96%) rename src/{lib => }/programs/__tests__/self-driving-detect.test.ts (98%) rename src/{lib => }/programs/__tests__/self-driving-prompt.test.ts (97%) rename src/{lib => }/programs/__tests__/source-maps-detect-agentic.test.ts (99%) rename src/{lib => }/programs/__tests__/source-maps-detect.test.ts (99%) rename src/{lib => }/programs/__tests__/source-maps-prompt.test.ts (97%) rename src/{lib => }/programs/__tests__/warehouse-ask-timeout.test.ts (94%) rename src/{lib => }/programs/__tests__/warehouse-seed-task.test.ts (94%) rename src/{lib => }/programs/__tests__/warehouse-suggestion.test.ts (96%) rename src/{lib => }/programs/__tests__/web-analytics-doctor-detect.test.ts (98%) rename src/{lib => }/programs/agent-skill/index.ts (92%) rename src/{lib => }/programs/agent-skill/steps.ts (87%) rename src/{lib => }/programs/ai-observability/index.ts (93%) rename src/{lib => }/programs/ai-observability/test/e2e.json (100%) rename src/{lib => }/programs/ai-opt-in-gate.ts (100%) rename src/{lib => }/programs/audit/detect.ts (100%) rename src/{lib => }/programs/audit/index.ts (95%) rename src/{lib => }/programs/audit/ledger-watcher.ts (100%) rename src/{lib => }/programs/audit/seed.ts (100%) rename src/{lib => }/programs/audit/test/e2e.json (100%) rename src/{lib => }/programs/audit/types.ts (100%) rename src/{lib => }/programs/authenticate.ts (98%) rename src/{lib => programs}/cloudflare-detection.ts (100%) rename src/{lib => }/programs/detect-map.ts (81%) rename src/{lib => programs}/detection/__tests__/agentic-progress.test.ts (100%) rename src/{lib => programs}/detection/__tests__/agentic.test.ts (99%) rename src/{lib => programs}/detection/__tests__/context.test.ts (96%) rename src/{lib => programs}/detection/__tests__/features.test.ts (98%) rename src/{lib => programs}/detection/__tests__/framework.test.ts (96%) rename src/{lib => programs}/detection/__tests__/package-manager.test.ts (99%) rename src/{lib => programs}/detection/__tests__/project-scope.test.ts (95%) rename src/{lib => programs}/detection/agentic.ts (99%) rename src/{lib => programs}/detection/context.ts (97%) rename src/{lib => programs}/detection/features.ts (100%) rename src/{lib => programs}/detection/framework.ts (95%) rename src/{lib => programs}/detection/index.ts (100%) rename src/{lib => programs}/detection/package-manager.ts (99%) rename src/{lib => programs}/detection/project-scope.ts (98%) rename src/{lib => }/programs/dispatch-family.ts (93%) rename src/{lib => }/programs/error-tracking-upload-source-maps/detect-agentic.ts (99%) rename src/{lib => }/programs/error-tracking-upload-source-maps/detect.ts (100%) rename src/{lib => }/programs/error-tracking-upload-source-maps/index.ts (94%) rename src/{lib => }/programs/error-tracking-upload-source-maps/prompt.ts (100%) rename src/{lib => }/programs/error-tracking-upload-source-maps/steps.ts (96%) rename src/{lib => }/programs/error-tracking-upload-source-maps/test/e2e.json (100%) rename src/{lib => }/programs/error-tracking/detect-agentic.ts (96%) rename src/{lib => }/programs/error-tracking/index.ts (92%) rename src/{lib => }/programs/error-tracking/test/e2e.json (100%) rename src/{lib => }/programs/events-audit/constants.ts (100%) rename src/{lib => }/programs/events-audit/index.ts (91%) rename src/{lib => }/programs/events-audit/seed.ts (96%) rename src/{lib => }/programs/events-audit/steps.ts (92%) rename src/{lib => programs}/framework-config.ts (100%) rename src/{ => programs}/frameworks/android/android-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/android/utils.ts (100%) rename src/{ => programs}/frameworks/angular/angular-wizard-agent.ts (94%) rename src/{ => programs}/frameworks/angular/utils.ts (100%) rename src/{ => programs}/frameworks/astro/astro-wizard-agent.ts (97%) rename src/{ => programs}/frameworks/astro/utils.ts (100%) rename src/{ => programs}/frameworks/django/django-wizard-agent.ts (95%) rename src/{ => programs}/frameworks/django/utils.ts (100%) rename src/{ => programs}/frameworks/elixir/elixir-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/fastapi/fastapi-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/fastapi/utils.ts (100%) rename src/{ => programs}/frameworks/flask/flask-wizard-agent.ts (95%) rename src/{ => programs}/frameworks/flask/utils.ts (100%) rename src/{ => programs}/frameworks/flutter/flutter-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/go/go-wizard-agent.ts (95%) rename src/{ => programs}/frameworks/java/java-wizard-agent.ts (97%) rename src/{ => programs}/frameworks/javascript-node/javascript-node-wizard-agent.ts (94%) rename src/{ => programs}/frameworks/javascript-web/javascript-web-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/javascript-web/utils.ts (100%) rename src/{ => programs}/frameworks/kmp/kmp-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/laravel/laravel-wizard-agent.ts (97%) rename src/{ => programs}/frameworks/laravel/utils.ts (100%) rename src/{ => programs}/frameworks/nextjs/nextjs-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/nextjs/utils.ts (100%) rename src/{ => programs}/frameworks/nuxt/nuxt-wizard-agent.ts (95%) rename src/{ => programs}/frameworks/python/python-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/python/utils.ts (100%) rename src/{ => programs}/frameworks/rails/rails-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/rails/utils.ts (100%) rename src/{ => programs}/frameworks/react-native/react-native-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/react-native/utils.ts (100%) rename src/{ => programs}/frameworks/react-router/react-router-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/react-router/utils.ts (100%) rename src/{ => programs}/frameworks/ruby/ruby-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/ruby/utils.ts (100%) rename src/{ => programs}/frameworks/rust/rust-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/svelte/svelte-wizard-agent.ts (93%) rename src/{ => programs}/frameworks/swift/__tests__/utils.test.ts (100%) rename src/{ => programs}/frameworks/swift/swift-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/swift/utils.ts (100%) rename src/{ => programs}/frameworks/tanstack-router/tanstack-router-wizard-agent.ts (96%) rename src/{ => programs}/frameworks/tanstack-router/utils.ts (100%) rename src/{ => programs}/frameworks/tanstack-start/tanstack-start-wizard-agent.ts (95%) rename src/{ => programs}/frameworks/tanstack-start/utils.ts (100%) rename src/{ => programs}/frameworks/vue/vue-wizard-agent.ts (93%) create mode 100644 src/programs/index.ts rename src/{lib => }/programs/mcp-analytics/index.ts (98%) rename src/{lib => }/programs/mcp/index.ts (98%) rename src/{lib => }/programs/metrics/index.ts (93%) rename src/{lib => }/programs/metrics/test/e2e.json (100%) rename src/{lib => }/programs/migration/index.ts (94%) rename src/{lib => }/programs/migration/steps.ts (84%) rename src/{lib => programs}/oauth/__tests__/program-scopes.test.ts (98%) rename src/{lib => programs}/oauth/program-scopes.ts (99%) rename src/{lib => }/programs/posthog-doctor/__tests__/doctor-schema.test.ts (95%) rename src/{lib => }/programs/posthog-doctor/fetch.ts (100%) rename src/{lib => }/programs/posthog-doctor/index.ts (91%) rename src/{lib => }/programs/posthog-doctor/kind-metadata.ts (100%) rename src/{lib => }/programs/posthog-doctor/steps.ts (81%) rename src/{lib => }/programs/posthog-doctor/types.ts (100%) rename src/{lib => }/programs/posthog-integration/__tests__/detect.test.ts (96%) rename src/{lib => }/programs/posthog-integration/__tests__/handoff.test.ts (100%) rename src/{lib => }/programs/posthog-integration/__tests__/index.test.ts (96%) rename src/{lib => }/programs/posthog-integration/constants.ts (100%) rename src/{lib => }/programs/posthog-integration/detect.ts (95%) rename src/{lib => }/programs/posthog-integration/handoff.ts (100%) rename src/{lib => }/programs/posthog-integration/index.ts (97%) rename src/{lib => }/programs/posthog-integration/steps.ts (94%) rename src/{lib => }/programs/posthog-integration/test/README.md (90%) rename src/{lib => }/programs/posthog-integration/test/e2e.json (100%) rename src/{lib => }/programs/program-registry.ts (97%) rename src/{lib => }/programs/program-run.ts (100%) rename src/{lib => }/programs/program-step.ts (99%) create mode 100644 src/programs/registry.ts rename src/{lib => }/programs/replay-vision/index.ts (94%) rename src/{lib => }/programs/replay-vision/test/e2e.json (100%) rename src/{lib => }/programs/revenue-analytics/detect.ts (97%) rename src/{lib => }/programs/revenue-analytics/index.ts (89%) rename src/{lib => }/programs/revenue-analytics/steps.ts (91%) rename src/{lib => }/programs/run-agent-legacy.ts (99%) rename src/{lib => }/programs/self-driving/ARCHITECTURE.md (98%) rename src/{lib => }/programs/self-driving/__tests__/step-keys.test.ts (100%) rename src/{lib => }/programs/self-driving/detect-agentic.ts (94%) rename src/{lib => }/programs/self-driving/detect.ts (98%) rename src/{lib => }/programs/self-driving/index.ts (94%) rename src/{lib => }/programs/self-driving/prompt.ts (99%) rename src/{lib => }/programs/self-driving/step-keys.ts (100%) rename src/{lib => }/programs/self-driving/steps.ts (95%) rename src/{lib => }/programs/self-driving/test/e2e.json (100%) rename src/{lib => }/programs/shared/health-check-step.ts (97%) rename src/{lib => }/programs/shared/package-scanning.ts (100%) rename src/{lib => }/programs/shared/posthog-cli-preinstall.ts (100%) rename src/{lib => }/programs/slack/index.ts (91%) rename src/{lib => programs}/task-stream/__tests__/audit-areas.test.ts (92%) rename src/{lib => programs}/task-stream/__tests__/event-plan-watcher.test.ts (97%) rename src/{lib => programs}/task-stream/__tests__/file-destination.test.ts (94%) rename src/{lib => programs}/task-stream/__tests__/posthog-destination.test.ts (99%) rename src/{lib => programs}/task-stream/__tests__/task-stream-push.test.ts (98%) rename src/{lib => programs}/task-stream/audit-areas.ts (96%) rename src/{lib => programs}/task-stream/destinations/file.ts (98%) rename src/{lib => programs}/task-stream/destinations/posthog.ts (99%) rename src/{lib => programs}/task-stream/event-plan-watcher.ts (100%) rename src/{lib => programs}/task-stream/index.ts (100%) rename src/{lib => programs}/task-stream/task-stream-push.ts (100%) rename src/{lib => programs}/task-stream/types.ts (100%) create mode 100644 src/programs/types.ts rename src/{lib => }/programs/warehouse-source/detect.ts (96%) rename src/{lib => }/programs/warehouse-source/index.ts (93%) rename src/{lib => }/programs/warehouse-source/steps.ts (95%) rename src/{lib => }/programs/warehouse-source/test/e2e.json (100%) rename src/{lib => programs}/warehouse-sources/__tests__/detect.test.ts (99%) rename src/{lib => programs}/warehouse-sources/detect.ts (99%) rename src/{lib => programs}/warehouse-sources/registry.ts (100%) rename src/{lib => programs}/warehouse-sources/types.ts (95%) rename src/{lib => }/programs/web-analytics-doctor/detect.ts (97%) rename src/{lib => }/programs/web-analytics-doctor/index.ts (96%) rename src/{lib => }/programs/web-analytics-doctor/steps.ts (70%) rename src/{agent => shared}/__tests__/agent-env-isolation.test.ts (99%) rename src/{agent => shared}/agent-env-isolation.ts (100%) rename src/{lib/programs/agent-skill/content => ui/tui/decks/agent-skill}/index.tsx (100%) rename src/{lib/programs/error-tracking-upload-source-maps/content => ui/tui/decks/error-tracking-upload-source-maps}/index.tsx (100%) rename src/{lib/programs/error-tracking/content => ui/tui/decks/error-tracking}/index.tsx (85%) rename src/{lib/programs/error-tracking/content => ui/tui/decks/error-tracking}/tips.ts (93%) rename src/{lib/programs/migration/content => ui/tui/decks/migration}/free-tier.tsx (100%) rename src/{lib/programs/migration/content => ui/tui/decks/migration}/index.tsx (96%) rename src/{lib/programs/migration/content => ui/tui/decks/migration}/pricing-structure.tsx (100%) rename src/{lib/programs/migration/content => ui/tui/decks/migration}/vendor-stack.tsx (100%) rename src/{lib/programs/posthog-integration/content => ui/tui/decks/posthog-integration}/data-flow.tsx (100%) rename src/{lib/programs/posthog-integration/content => ui/tui/decks/posthog-integration}/funnel.tsx (100%) rename src/{lib/programs/posthog-integration/content => ui/tui/decks/posthog-integration}/index.tsx (100%) rename src/{lib/programs/posthog-integration/content => ui/tui/decks/posthog-integration}/line-chart.tsx (100%) rename src/{lib/programs/posthog-integration/content => ui/tui/decks/posthog-integration}/product-suite.tsx (100%) rename src/{lib/programs/revenue-analytics/content => ui/tui/decks/revenue-analytics}/index.tsx (74%) rename src/{lib/programs/self-driving/content => ui/tui/decks/self-driving}/index.tsx (100%) rename src/{lib/programs/self-driving/content => ui/tui/decks/self-driving}/pipeline-diagram.tsx (100%) rename src/{lib/programs/self-driving/content => ui/tui/decks/self-driving}/pricing.ts (100%) rename src/{lib/programs/self-driving/content => ui/tui/decks/self-driving}/tips.ts (100%) rename src/{lib/programs/warehouse-source/content => ui/tui/decks/warehouse-source}/index.tsx (73%) diff --git a/.claude/skills/adding-framework-support/SKILL.md b/.claude/skills/adding-framework-support/SKILL.md index 671b0ad24..625e7c562 100644 --- a/.claude/skills/adding-framework-support/SKILL.md +++ b/.claude/skills/adding-framework-support/SKILL.md @@ -20,8 +20,8 @@ creating its own runner or changing existing routing defaults. ## Extend the framework configuration -Start with [FrameworkConfig](../../../src/lib/framework-config.ts) and a nearby -example under [src/frameworks](../../../src/frameworks/). Framework-specific +Start with [FrameworkConfig](../../../src/programs/framework-config.ts) and a nearby +example under [src/programs/frameworks](../../../src/programs/frameworks/). Framework-specific detection, context, environment conventions, and UI metadata belong here. Integration instructions and examples belong in context-mill. @@ -29,16 +29,16 @@ Integration instructions and examples belong in context-mill. order controls first-match detection and the framework picker. Keep specific frameworks before language fallbacks and generic Node last; preserve the overlap rules in the - [detection checks](../../../src/lib/detection/__tests__/framework.test.ts). -2. Add the config under `src/frameworks//-wizard-agent.ts`. Use a + [detection checks](../../../src/programs/detection/__tests__/framework.test.ts). +2. Add the config under `src/programs/frameworks//-wizard-agent.ts`. Use a `type` for framework context so it satisfies `Record`. Export the config; the integration program already supplies execution. -3. Import the config into [FRAMEWORK_REGISTRY](../../../src/lib/registry.ts). +3. Import the config into [FRAMEWORK_REGISTRY](../../../src/programs/registry.ts). The display label comes from `metadata.name`. Read the current interface for the complete required fields. In particular, `detection.detectPackageManager` is required: reuse an adapter from -[package-manager detection](../../../src/lib/detection/package-manager.ts). Use +[package-manager detection](../../../src/programs/detection/package-manager.ts). Use `metadata.setup.questions` for unresolved project variants; `gatherContext` collects framework context. Optional notices and extra MCP servers also belong in metadata. @@ -46,7 +46,7 @@ in metadata. Use `usesPackageJson: false` for frameworks without a package.json dependency. Their required `getVersion` callback can return `undefined`. Minimum-version checking requires both `minimumVersion` and `getInstalledVersion`; unknown -versions pass. [Context detection](../../../src/lib/detection/context.ts) +versions pass. [Context detection](../../../src/programs/detection/context.ts) returns unsupported-version data for the integration UI rather than aborting itself. @@ -54,10 +54,10 @@ itself. | Starting point | Pattern to reuse | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| [Next.js](../../../src/frameworks/nextjs/) | `hasDeclaredDependency` from `utils/package-json`, `tryGetPackageJson` from `utils/setup-utils`, and router setup questions | -| [Django](../../../src/frameworks/django/) | Python project files, context gathering, and Python package-manager detection | -| [Laravel](../../../src/frameworks/laravel/) | Composer and framework-specific filesystem signals | -| [Rails](../../../src/frameworks/rails/) | Gemfile detection and Ruby conventions | +| [Next.js](../../../src/programs/frameworks/nextjs/) | `hasDeclaredDependency` from `utils/package-json`, `tryGetPackageJson` from `utils/setup-utils`, and router setup questions | +| [Django](../../../src/programs/frameworks/django/) | Python project files, context gathering, and Python package-manager detection | +| [Laravel](../../../src/programs/frameworks/laravel/) | Composer and framework-specific filesystem signals | +| [Rails](../../../src/programs/frameworks/rails/) | Gemfile detection and Ruby conventions | Use [bounded filesystem helpers](../../../src/shared/utils/bounded-fs.ts) for project scans and reads. They bound traversal and skip dependency/build directories; add @@ -88,7 +88,7 @@ content-mill variants. For an end-to-end run, use a disposable test app and the [exploration guide](../exploring-the-wizard/SKILL.md). For prompt, environment-upload, or outro changes, inspect the current -[integration program](../../../src/lib/programs/posthog-integration/) and the +[integration program](../../../src/programs/posthog-integration/) and the selected sequence. Some fields remain in the interface without a current consumer: `getOutroNextSteps` is not used by the integration outro. Linear post-run/outro hooks are not shared by the orchestrator; see the diff --git a/.claude/skills/adding-skill-program/SKILL.md b/.claude/skills/adding-skill-program/SKILL.md index 60c3f8fb3..3aedd8778 100644 --- a/.claude/skills/adding-skill-program/SKILL.md +++ b/.claude/skills/adding-skill-program/SKILL.md @@ -32,10 +32,10 @@ them. [Context-mill](https://github.com/PostHog/context-mill) owns skill content and `cliEntries`. A new skill-backed child of an existing family ships through context-mill; inspect - [family dispatch](../../../src/lib/programs/dispatch-family.ts). Unpromoted + [family dispatch](../../../src/programs/dispatch-family.ts). Unpromoted skills run through [the skill command](../../../src/commands/skill.ts). - **Native program:** use a - [ProgramConfig](../../../src/lib/programs/program-step.ts) when the wizard + [ProgramConfig](../../../src/programs/program-step.ts) when the wizard needs its own flow, screens, detection, composition, or other native behavior. Keep product instructions in context-mill. @@ -45,12 +45,12 @@ is `id`, not the retired `flowKey`. ## Build a native orchestrator program -Use [metrics](../../../src/lib/programs/metrics/) as the current Pi/orchestrator +Use [metrics](../../../src/programs/metrics/) as the current Pi/orchestrator example and read the [runner architecture](../wizard-development/references/ARCHITECTURE.md) when changing execution behavior. -1. Add the program config under `src/lib/programs//`. Set `agentFlow` when +1. Add the program config under `src/programs//`. Set `agentFlow` when its content-mill flow differs from `id`; setting it explicitly also documents the content dependency. Keep a `run` definition so the outer runner executes agent work. @@ -60,7 +60,7 @@ changing execution behavior. loads `agentFlow ?? id`, requires a seed prompt, and checks task-skill variants before running. `run.skillId` alone does not define this flow. 3. Register the config in - [PROGRAM_REGISTRY](../../../src/lib/programs/program-registry.ts) and add its + [PROGRAM_REGISTRY](../../../src/programs/program-registry.ts) and add its Pi/orchestrator entry to [PROGRAM_BINDINGS](../../../src/agent/runner/switchboard/index.ts). [Existing binding checks](../../../src/agent/runner/__tests__/switchboard.test.ts) @@ -70,7 +70,7 @@ changing execution behavior. and register it in [bin.ts](../../../bin.ts). A native family child uses the handlers in family dispatch. Program registration derives screen sequences and store lookup, not the top-level CLI `.use()` chain. -5. Check [program OAuth scopes](../../../src/lib/oauth/program-scopes.ts) +5. Check [program OAuth scopes](../../../src/programs/oauth/program-scopes.ts) against the tools the program needs; add scopes only when the base set is insufficient. @@ -82,13 +82,13 @@ or changing gateway-required prompt material. ## Simple linear programs and existing flows For a very simple linear flow, use -[createSkillProgram](../../../src/lib/programs/agent-skill/index.ts) to +[createSkillProgram](../../../src/programs/agent-skill/index.ts) to configure installation of one skill. Register the native program as above with an explicit Pi/linear binding; the factory does not select a sequence. Read `SkillProgramOptions` for required fields; -[audit](../../../src/lib/programs/audit/) demonstrates factory customization and +[audit](../../../src/programs/audit/) demonstrates factory customization and a dynamic `run(session)` that seeds a ledger. -[Revenue analytics](../../../src/lib/programs/revenue-analytics/) builds its +[Revenue analytics](../../../src/programs/revenue-analytics/) builds its config directly and adds prerequisite detection. `ProgramRun.customPrompt`, `abortCases`, `postRun`, and `buildOutroData` are @@ -102,9 +102,9 @@ does not preserve these behaviors automatically. ## Screens, prerequisites, and composition -Reuse [AGENT_SKILL_STEPS](../../../src/lib/programs/agent-skill/steps.ts): +Reuse [AGENT_SKILL_STEPS](../../../src/programs/agent-skill/steps.ts): intro, health check, auth, run, outro, and keep-skills. Auth also applies the -shared [AI opt-in gate](../../../src/lib/programs/ai-opt-in-gate.ts) for agent +shared [AI opt-in gate](../../../src/programs/ai-opt-in-gate.ts) for agent programs. Override `screenId`, not `screen`, when adapting a step. New screens need an entry in [ScreenId](../../../src/ui/tui/screen-sequences.ts), a component, and registration in @@ -123,12 +123,12 @@ prerequisite strategy. `requires` currently records metadata; it does not execute or enforce prior programs. Compose real work through `ProgramStep.run`, with `onRunPrep` and `targetDir` when needed. The -[integration run step](../../../src/lib/programs/posthog-integration/index.ts) -and [self-driving](../../../src/lib/programs/self-driving/) demonstrate this. +[integration run step](../../../src/programs/posthog-integration/index.ts) +and [self-driving](../../../src/programs/self-driving/) demonstrate this. Composed sub-runs are structurally linear; orchestrators cannot nest. A host run step without `run` can also set `targetDir` and `onRunPrep` to scope the program's own agent to a picked project and keep its sequence, as -[error-tracking](../../../src/lib/programs/error-tracking/) does. +[error-tracking](../../../src/programs/error-tracking/) does. ## Validate the affected path diff --git a/.claude/skills/exploring-the-wizard/SKILL.md b/.claude/skills/exploring-the-wizard/SKILL.md index 2dc754f74..4ac32d73f 100644 --- a/.claude/skills/exploring-the-wizard/SKILL.md +++ b/.claude/skills/exploring-the-wizard/SKILL.md @@ -129,5 +129,5 @@ The shared log is `/tmp/posthog-wizard.log`. Record its byte count before a run and read from that count plus one afterward. Run sweeps serially so their logs remain attributable. `read_state` omits `frameworkContext`; an empty `setupQuestions` list alone does not prove a router mode. When necessary, -inspect the detector under [`src/frameworks/`](../../../src/frameworks/) against +inspect the detector under [`src/programs/frameworks/`](../../../src/programs/frameworks/) against the same fixture. diff --git a/.claude/skills/ink-tui/SKILL.md b/.claude/skills/ink-tui/SKILL.md index 590640ee6..d7a1f2247 100644 --- a/.claude/skills/ink-tui/SKILL.md +++ b/.claude/skills/ink-tui/SKILL.md @@ -40,7 +40,7 @@ conventions rather than copying upstream component manuals. 3. Register the component in [screen-registry.tsx](../../../src/ui/tui/screen-registry.tsx). 4. Reference it through `screenId` in the owning - [program's steps](../../../src/lib/programs/), with the appropriate + [program's steps](../../../src/programs/), with the appropriate visibility, completion, and gate predicates. Screen sequences derive from program steps. Do not hand-maintain a second diff --git a/.claude/skills/ink-tui/references/ARCHITECTURE.md b/.claude/skills/ink-tui/references/ARCHITECTURE.md index fdf59395c..9521fac74 100644 --- a/.claude/skills/ink-tui/references/ARCHITECTURE.md +++ b/.claude/skills/ink-tui/references/ARCHITECTURE.md @@ -5,7 +5,7 @@ | Surface | Owns | | ----------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [WizardSession](../../../../src/lib/wizard-session.ts) | Run configuration, decisions, credentials, and lifecycle state | -| [ProgramStep](../../../../src/lib/programs/program-step.ts) | Screens, visibility/completion predicates, gates, and initialization hooks | +| [ProgramStep](../../../../src/programs/program-step.ts) | Screens, visibility/completion predicates, gates, and initialization hooks | | [screen-sequences.ts](../../../../src/ui/tui/screen-sequences.ts) | `ScreenId`, `Screen`, `Sequence`, and the derived `PROGRAM_SEQUENCES` | | [WizardRouter](../../../../src/ui/tui/router.ts) | Resolution and the `Overlay` stack | | [WizardStore](../../../../src/ui/tui/store.ts) | Reactive state, gate promises, display observations, and pending interactions | @@ -17,7 +17,7 @@ entries: `id`, `show`, and `isComplete`. Completion defaults to the step's `gate` when no separate `isComplete` is provided. Steps without a screen are omitted, and the exit screen is appended. The projection applies -[withAiOptInGate](../../../../src/lib/programs/ai-opt-in-gate.ts) consistently +[withAiOptInGate](../../../../src/programs/ai-opt-in-gate.ts) consistently with store gate creation. `WizardRouter.resolve(session)` first checks the overlay stack, then returns the diff --git a/.claude/skills/wizard-development/SKILL.md b/.claude/skills/wizard-development/SKILL.md index cde67b09b..5fee5c7a5 100644 --- a/.claude/skills/wizard-development/SKILL.md +++ b/.claude/skills/wizard-development/SKILL.md @@ -19,9 +19,9 @@ infrastructure should consume those boundaries. | Concern | Owner | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Framework detection, context, env conventions | [FrameworkConfig](../../../src/lib/framework-config.ts) and [framework configs](../../../src/frameworks/) | +| Framework detection, context, env conventions | [FrameworkConfig](../../../src/programs/framework-config.ts) and [framework configs](../../../src/programs/frameworks/) | | Integration instructions and orchestrator flows/tasks | [context-mill](https://github.com/PostHog/context-mill) | -| Programs, steps, prerequisites and outcomes | [programs](../../../src/lib/programs/) | +| Programs, steps, prerequisites and outcomes | [programs](../../../src/programs/) | | Sequence, harness, model and effort selection | [switchboard](../../../src/agent/runner/switchboard/) | | Local tool permissions and scanner adapters | [agent-interface](../../../src/agent/agent-interface.ts), [YARA hooks](../../../src/agent/yara-hooks.ts), [Pi security](../../../src/agent/runner/harness/pi/security.ts) | | Scanner rules | [warlock](https://github.com/PostHog/warlock) | @@ -34,7 +34,7 @@ infrastructure should consume those boundaries. are separate: Pi supports gateway-backed Anthropic and OpenAI transports. - **Prefer orchestration.** A seed agent plans work, then task agents execute focused conversations. Start from - [metrics](../../../src/lib/programs/metrics/) and its context-mill flow. + [metrics](../../../src/programs/metrics/) and its context-mill flow. - **Linear is for very simple tasks and legacy support.** Composed program sub-runs are also structurally clamped to linear; orchestration cannot nest through that seam. diff --git a/.claude/skills/wizard-development/references/ARCHITECTURE.md b/.claude/skills/wizard-development/references/ARCHITECTURE.md index c29badcf2..570d1e5e1 100644 --- a/.claude/skills/wizard-development/references/ARCHITECTURE.md +++ b/.claude/skills/wizard-development/references/ARCHITECTURE.md @@ -54,7 +54,7 @@ signatures. Do not migrate a linear program merely by changing its binding if it depends on these hooks. Inspect the orchestrator's flow and completion path instead. -[Metrics](../../../../src/lib/programs/metrics/) is a current Pi/orchestrator +[Metrics](../../../../src/programs/metrics/) is a current Pi/orchestrator example. Native command modules still need registration in [bin.ts](../../../../bin.ts); screen sequences derive from the program registry. diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 77f26a9b9..3a1795f54 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -34,7 +34,7 @@ module.exports = { // The agent surface. It takes resolved data in, reports through // progress events and asks through an injected answerer, so nothing // here may reach a UI, the session, detection, the CLI or a program at - // runtime. Program types stay importable until B1 moves + // runtime. ProgramId remains a type-only exception until B2 moves // PROGRAM_BINDINGS to programs. files: ['src/agent/**/*.ts'], excludedFiles: ['**/__tests__/**'], @@ -63,16 +63,13 @@ module.exports = { '@ui/**', '**/ui/**', '@lib/**', - '!@lib/programs/**', '**/lib/**', - '!**/lib/programs/**', '**/wizard-session', '**/detection/**', '**/runners/**', '**/commands/**', '@steps', '@steps/**', - '@frameworks/**', '**/frameworks/**', '@utils/setup-utils', '@shared/utils/setup-utils', @@ -85,10 +82,10 @@ module.exports = { 'The agent reports through progress events and asks through AgentInteraction; it takes everything else through RunConfig and RunInput.', }, { - group: ['@lib/programs/**', '**/programs/**'], + group: ['@programs/**', '**/programs/**'], allowTypeImports: true, message: - 'The agent takes program data through RunConfig. Types only, until B1 moves PROGRAM_BINDINGS to programs.', + 'The agent takes program data through RunConfig. ProgramId types remain until B2 moves PROGRAM_BINDINGS to programs.', }, ], }, diff --git a/AGENTS.md b/AGENTS.md index ad66039db..773faf9cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ configuration surfaces. Each domain has a dedicated boundary: -- **Frameworks** → `FrameworkConfig` in `src/frameworks//` +- **Frameworks** → `FrameworkConfig` in `src/programs/frameworks//` - **Integration knowledge** → markdown skills in the [context-mill](https://github.com/PostHog/context-mill) repo - **Security policy** → YARA-X rules in the @@ -34,8 +34,10 @@ Each domain has a dedicated boundary: `@agent/types` (types); see [src/agent/README.md](src/agent/README.md) - **Shared** → `src/shared/`, stateless library code with no upward imports; see [src/shared/README.md](src/shared/README.md) -- **Programs** → step arrays in `src/lib/programs/` -- **TUI** → screen components and primitives in `src/ui/tui/` +- **Programs** → configs, detection, framework registry and task stream in + `src/programs/`; runtime and type entries are `@programs` and + `@programs/types` +- **TUI** → screens, primitives and content decks in `src/ui/tui/` Adding a new concern means finding the narrowest existing surface, not adding logic to the runner. Keep changes local to the boundary that owns them. @@ -131,7 +133,7 @@ confuse it with the top-level `wizard skill` command. ([`src/commands/factories/native-command-factory.ts`](src/commands/factories/native-command-factory.ts)). - **Family commands** (e.g. `audit`) resolve subcommands at runtime against the `cliEntries` in `skill-menu.json`. Logic lives in - [`src/lib/programs/dispatch-family.ts`](src/lib/programs/dispatch-family.ts). + [`src/programs/dispatch-family.ts`](src/programs/dispatch-family.ts). Adding a skill-backed subcommand is a **context-mill** release, not a wizard change. diff --git a/README.md b/README.md index b4756f113..be133cf7b 100644 --- a/README.md +++ b/README.md @@ -156,13 +156,13 @@ route review to their owning team instead. |---|---| | `*` (everything else, including all other programs) | `@PostHog/team-wizard-docs` | | `src/agent/` | `@PostHog/team-wizard-docs` | -| `src/lib/programs/posthog-integration/` | `@PostHog/team-wizard-docs` | -| `src/lib/programs/error-tracking-upload-source-maps/` | `@PostHog/team-error-tracking` | -| `src/lib/programs/mcp-analytics/` | `@PostHog/team-mcp-analytics` | -| `src/lib/programs/revenue-analytics/` | `@PostHog/team-web-analytics` | -| `src/lib/programs/self-driving/` | `@PostHog/team-self-driving` | -| `src/lib/programs/warehouse-source/` | `@PostHog/team-warehouse-sources` | -| `src/lib/programs/web-analytics-doctor/` | `@PostHog/team-web-analytics` | +| `src/programs/posthog-integration/` | `@PostHog/team-wizard-docs` | +| `src/programs/error-tracking-upload-source-maps/` | `@PostHog/team-error-tracking` | +| `src/programs/mcp-analytics/` | `@PostHog/team-mcp-analytics` | +| `src/programs/revenue-analytics/` | `@PostHog/team-web-analytics` | +| `src/programs/self-driving/` | `@PostHog/team-self-driving` | +| `src/programs/warehouse-source/` | `@PostHog/team-warehouse-sources` | +| `src/programs/web-analytics-doctor/` | `@PostHog/team-web-analytics` | Ownership is by directory. Programs not listed above (`agent-skill`, `audit`, `events-audit`, `mcp`, `migration`, `posthog-doctor`, @@ -273,7 +273,7 @@ health_issue:read wizard_session:read wizard_session:write The source of truth is `WIZARD_OAUTH_SCOPES` in `src/shared/constants.ts`, which documents why each scope is needed — if this block drifts, trust the code. Some programs request more on top (`PROGRAM_SCOPE_ADDITIONS` in -`src/lib/oauth/program-scopes.ts`); the default integration flow adds +`src/programs/oauth/program-scopes.ts`); the default integration flow adds `integration:read` and `external_data_source:read` / `external_data_source:write`. @@ -281,7 +281,7 @@ Some programs request more on top (`PROGRAM_SCOPE_ADDITIONS` in The wizard's OAuth app on the PostHog side caps the scopes its tokens may carry (`OAuthApplication.scopes`). Any scope requested in this repo (see -`src/lib/oauth/program-scopes.ts`) must be grantable under that ceiling, or +`src/programs/oauth/program-scopes.ts`) must be grantable under that ceiling, or `/authorize` drops it and the call that needs it 403s. **A granted token can be narrower than the request even with a correct @@ -507,11 +507,13 @@ Path aliases defined in `tsconfig.build.json`, resolved by tsdown: | `@agent` | `src/agent/index.ts`, the agent's runtime entry; the only agent import outside `src/agent` besides types | | `@agent/types` | `src/agent/types.ts`, type-only | | `@agent/*` | `src/agent/*`, inside `src/agent` and its tests only | +| `@programs` | `src/programs/index.ts`, the programs runtime entry | +| `@programs/types` | `src/programs/types.ts`, type-only | +| `@programs/*` | `src/programs/*`, retained for internal imports and tracked migration edges | | `@shared/*` | `src/shared/*` | | `@utils/*` | `src/shared/utils/*` | | `@ui/*` | `src/ui/*` | | `@steps/*` | `src/steps/*` | -| `@frameworks/*` | `src/frameworks/*` | ## Running locally diff --git a/docs/error-catalog.md b/docs/error-catalog.md index c162e2897..ef7592a48 100644 --- a/docs/error-catalog.md +++ b/docs/error-catalog.md @@ -11,7 +11,7 @@ classify failures without parsing human-readable messages. [`src/shared/errors/catalog.ts`](../src/shared/errors/catalog.ts) - **Consumers:** `wizardAbort()` ([`src/shared/utils/wizard-abort.ts`](../src/shared/utils/wizard-abort.ts)), the task stream - ([`src/lib/task-stream/`](../src/lib/task-stream/)), and non-interactive hosts + ([`src/programs/task-stream/`](../src/programs/task-stream/)), and non-interactive hosts reading stderr. ## Stability contract @@ -117,7 +117,7 @@ guarantee. Program detect steps write `{ kind, ...detail }` into `session.frameworkContext.detectError`. `detectErrorCode()` -([`src/lib/programs/detect-map.ts`](../src/lib/programs/detect-map.ts)) maps `kind` +([`src/programs/detect-map.ts`](../src/programs/detect-map.ts)) maps `kind` → code, and the whole object — `kind` included — rides along as `OutroData.errorDetail`. diff --git a/docs/runbooks/agentic-monorepo-detection.md b/docs/runbooks/agentic-monorepo-detection.md index 36fbd8664..f342de891 100644 --- a/docs/runbooks/agentic-monorepo-detection.md +++ b/docs/runbooks/agentic-monorepo-detection.md @@ -35,10 +35,10 @@ it. - Only non-interactive basic-integration runs — headless and `--ci`. Interactive runs have their own detect step and never enter it. -- Phase: `scopeInstallDirToProject` in `src/lib/detection/project-scope.ts`, +- Phase: `scopeInstallDirToProject` in `src/programs/detection/project-scope.ts`, called from the top of `ciPreRun` in - `src/lib/programs/posthog-integration/index.ts`. -- Detector: `detectProjectsWithAgent` in `src/lib/detection/agentic.ts`; + `src/programs/posthog-integration/index.ts`. +- Detector: `detectProjectsWithAgent` in `src/programs/detection/agentic.ts`; self-driving uses the same detector with its own chooser. - Each run fires one `wizard: agentic detection` event tagged with the outcome (`flag-off | error | timeout | no-project | recommended | first-instrumentable`). diff --git a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts index 272105bc6..ac9d7b85c 100644 --- a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts +++ b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts @@ -19,15 +19,15 @@ import { setUI } from '@ui/index'; import { buildSession, RunPhase } from '@lib/wizard-session'; import { Integration } from '@shared/constants'; import { HostResolution } from '@shared/host-resolution'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { Program, getProgramConfig, type ProgramId, -} from '@lib/programs/program-registry'; +} from '@programs'; import { ScreenId } from '@ui/tui/router'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; import { WizardCiDriver } from '../wizard-ci-driver'; import { decideE2eAction, type WizardE2eProfile } from '../e2e-profile'; import { profileFor } from '../profiles'; diff --git a/e2e-harness/__tests__/e2e-profile-ask.test.ts b/e2e-harness/__tests__/e2e-profile-ask.test.ts index 5bc8fdf97..447d21e0b 100644 --- a/e2e-harness/__tests__/e2e-profile-ask.test.ts +++ b/e2e-harness/__tests__/e2e-profile-ask.test.ts @@ -18,7 +18,7 @@ import { type WizardE2eProfile, } from '../e2e-profile'; import { profileFor, resolveE2eProfile } from '../profiles'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import type { CiState } from '../wizard-ci-driver'; const text = (id: string, prompt = id): AskQuestion => ({ diff --git a/e2e-harness/__tests__/e2e-result.test.ts b/e2e-harness/__tests__/e2e-result.test.ts index cb03ec971..753e5722c 100644 --- a/e2e-harness/__tests__/e2e-result.test.ts +++ b/e2e-harness/__tests__/e2e-result.test.ts @@ -13,7 +13,7 @@ import os from 'os'; import path from 'path'; import { OutroKind, RunPhase } from '@lib/wizard-session'; import type { AskQuestion, WizardSession } from '@lib/wizard-session'; -import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@lib/programs/warehouse-source/detect'; +import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect'; import { Overlay } from '@ui/tui/router'; import { E2eRunRecorder, diff --git a/e2e-harness/__tests__/wizard-ci-driver.test.ts b/e2e-harness/__tests__/wizard-ci-driver.test.ts index 7462b213f..a634ff9c5 100644 --- a/e2e-harness/__tests__/wizard-ci-driver.test.ts +++ b/e2e-harness/__tests__/wizard-ci-driver.test.ts @@ -15,13 +15,13 @@ import { setUI } from '@ui/index'; import { buildSession, RunPhase, McpOutcome } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { ScreenId, Overlay } from '@ui/tui/router'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { WizardCiDriver, UnknownActionError } from '../wizard-ci-driver'; import { ACTION_REGISTRY, NO_ACTION_SCREENS } from '../action-registry'; -import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/index'; +import { SOURCE_MAPS_CONTEXT_KEYS } from '@programs/error-tracking-upload-source-maps/index'; import { OutroKind } from '@lib/wizard-session'; function freshStore(): WizardStore { diff --git a/e2e-harness/action-registry.ts b/e2e-harness/action-registry.ts index 63c48870c..2449deee4 100644 --- a/e2e-harness/action-registry.ts +++ b/e2e-harness/action-registry.ts @@ -19,11 +19,11 @@ import type { AskAnswers } from '@lib/wizard-session'; import { SOURCE_MAPS_CONTEXT_KEYS, VARIANT_DISPLAY_NAME, -} from '@lib/programs/error-tracking-upload-source-maps/index'; +} from '@programs/error-tracking-upload-source-maps/index'; import { GITHUB_REQUIRED_BODY, GITHUB_REQUIRED_MESSAGE, -} from '@lib/programs/self-driving/detect'; +} from '@programs/self-driving/detect'; /** One commit action legal on a given screen. */ export interface DriverAction { diff --git a/e2e-harness/e2e-result.ts b/e2e-harness/e2e-result.ts index e34a52dfd..9240935eb 100644 --- a/e2e-harness/e2e-result.ts +++ b/e2e-harness/e2e-result.ts @@ -22,8 +22,8 @@ import fs from 'fs'; import path from 'path'; import { OutroKind, type WizardSession } from '@lib/wizard-session'; -import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@lib/programs/warehouse-source/detect'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; import type { E2eDecisionReport } from './e2e-profile.js'; /** One `wizard_ask` batch the run was shown. */ diff --git a/e2e-harness/profiles.ts b/e2e-harness/profiles.ts index 3cc126d33..d0c343e6e 100644 --- a/e2e-harness/profiles.ts +++ b/e2e-harness/profiles.ts @@ -3,7 +3,7 @@ * program's flow. * * Each program declares its test path as JSON next to it - * (`src/lib/programs//test/e2e.json`): a `profile` (the options the run + * (`src/programs//test/e2e.json`): a `profile` (the options the run * auto-takes) plus a documented `path`. {@link profileFor} loads the `profile` * and maps it by program id. * @@ -11,7 +11,7 @@ * so `decideE2eAction` stays a pure function of (state, profile). */ -import { Program, type ProgramId } from '@lib/programs/program-registry'; +import { Program, type ProgramId } from '@programs'; import { DEFAULT_E2E_PROFILE, DEFAULT_E2E_VARIATION, @@ -19,15 +19,15 @@ import { type WizardE2eProfile, type WizardE2eVariation, } from './e2e-profile.js'; -import posthogIntegrationE2e from '@lib/programs/posthog-integration/test/e2e.json'; -import aiObservabilityE2e from '@lib/programs/ai-observability/test/e2e.json'; -import metricsE2e from '@lib/programs/metrics/test/e2e.json'; -import replayVisionE2e from '@lib/programs/replay-vision/test/e2e.json'; -import selfDrivingE2e from '@lib/programs/self-driving/test/e2e.json'; -import sourceMapsE2e from '@lib/programs/error-tracking-upload-source-maps/test/e2e.json'; -import errorTrackingE2e from '@lib/programs/error-tracking/test/e2e.json'; -import warehouseSourceE2e from '@lib/programs/warehouse-source/test/e2e.json'; -import auditE2e from '@lib/programs/audit/test/e2e.json'; +import posthogIntegrationE2e from '@programs/posthog-integration/test/e2e.json'; +import aiObservabilityE2e from '@programs/ai-observability/test/e2e.json'; +import metricsE2e from '@programs/metrics/test/e2e.json'; +import replayVisionE2e from '@programs/replay-vision/test/e2e.json'; +import selfDrivingE2e from '@programs/self-driving/test/e2e.json'; +import sourceMapsE2e from '@programs/error-tracking-upload-source-maps/test/e2e.json'; +import errorTrackingE2e from '@programs/error-tracking/test/e2e.json'; +import warehouseSourceE2e from '@programs/warehouse-source/test/e2e.json'; +import auditE2e from '@programs/audit/test/e2e.json'; const PROFILES: Partial> = { [Program.PostHogIntegration]: diff --git a/package.json b/package.json index 4df515d69..e4ecd50eb 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "test": "pnpm build && vitest run", "test:legacy": "vitest run --project legacy", "test:agent": "vitest run --project agent", + "test:programs": "vitest run --project programs", "test:tui": "vitest run --project tui", "test:cli": "vitest run --project cli", "test:harness": "vitest run --project harness", diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 75b246b8e..c34f718eb 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -22,27 +22,27 @@ import { Program, getProgramConfig, type ProgramId, -} from '@lib/programs/program-registry'; +} from '@programs'; import type { Harness, Sequence } from '@shared/constants'; import { buildSession } from '@lib/wizard-session'; import { initLocalDev } from '@shared/local-dev'; import { configureGatewayFromCIEnvironment } from '@agent/gateway-session'; -import { runProgramAgent } from '@lib/programs/run-agent-legacy'; -import { TaskStreamPush, createFileDestination } from '@lib/task-stream/index'; -import { getAuditChecks } from '@lib/programs/audit/types'; -import { authenticate } from '@lib/programs/authenticate'; +import { runProgramAgent } from '@programs/run-agent-legacy'; +import { TaskStreamPush, createFileDestination } from '@programs/task-stream/index'; +import { getAuditChecks } from '@programs/audit/types'; +import { authenticate } from '@programs/authenticate'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { logToFile } from '@utils/debug'; import { join } from 'path'; -import { detectFramework } from '@lib/detection/index'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { detectFramework } from '@programs/detection/index'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import type { Integration } from '@shared/constants'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; -import { ERROR_TRACKING_PROJECT_PATH_KEY } from '@lib/programs/error-tracking/detect-agentic'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; +import { ERROR_TRACKING_PROJECT_PATH_KEY } from '@programs/error-tracking/detect-agentic'; import { detectSourceMapsPrerequisites, SOURCE_MAPS_CONTEXT_KEYS, -} from '@lib/programs/error-tracking-upload-source-maps/index'; +} from '@programs/error-tracking-upload-source-maps/index'; import { ScreenId, Overlay } from '@ui/tui/router'; import { WizardCiDriver } from '@e2e-harness/wizard-ci-driver'; import { diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index 9ba7d9236..c53cc623a 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -2,7 +2,14 @@ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; -export type Surface = 'env' | 'shared' | 'legacy' | 'agent' | 'tui' | 'cli'; +export type Surface = + | 'env' + | 'shared' + | 'legacy' + | 'agent' + | 'programs' + | 'tui' + | 'cli'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(HERE, '../../..'); @@ -12,15 +19,13 @@ const SURFACE_RULES: ReadonlyArray boolean]> = ['env', (p) => p === 'src/env.ts'], ['shared', (p) => p.startsWith('src/shared/')], ['agent', (p) => p.startsWith('src/agent/')], + ['programs', (p) => p.startsWith('src/programs/')], [ 'tui', (p) => - p.startsWith('src/ui/tui/') || - p === 'src/ui/logging-ui.ts' || - p === 'src/ui/headless-ui.ts' || - p === 'src/commands/factories/family-picker.tsx' || - /^src\/lib\/programs\/[^/]+\/content\//.test(p) || - /^src\/lib\/programs\/[^/]+\/tips\.ts$/.test(p), + p.startsWith('src/ui/') || + p.startsWith('src/steps/') || + p === 'src/commands/factories/family-picker.tsx', ], [ 'cli', @@ -43,17 +48,20 @@ export function classifySurface(relPath: string): Surface { export const ALLOWED_IMPORTS: Record = { env: [], shared: ['env', 'shared'], - legacy: ['env', 'shared', 'legacy'], - // Program types stay importable from the agent until B1 moves the bindings. - agent: ['env', 'shared', 'legacy', 'agent'], - tui: ['env', 'shared', 'legacy', 'tui'], - cli: ['env', 'shared', 'legacy', 'agent', 'tui', 'cli'], + legacy: ['env', 'shared', 'legacy', 'programs'], + // B2 moves bindings and removes the agent's remaining ProgramId type imports. + agent: ['env', 'shared', 'agent'], + programs: ['env', 'shared', 'agent', 'programs'], + tui: ['env', 'shared', 'legacy', 'programs', 'tui'], + cli: ['env', 'shared', 'legacy', 'agent', 'programs', 'tui', 'cli'], }; // The agent's public entries. Outside `src/agent`, an import into the agent // must land on one of these; `types.ts` is type-only, so the TUI may take it. const AGENT_VALUES_ENTRY = 'src/agent/index.ts'; const AGENT_TYPES_ENTRY = 'src/agent/types.ts'; +const PROGRAMS_VALUES_ENTRY = 'src/programs/index.ts'; +const PROGRAMS_TYPES_ENTRY = 'src/programs/types.ts'; /** The rule an edge breaks, or null when it is allowed. */ export function ruleFor(fromFile: string, toFile: string): string | null { @@ -69,6 +77,12 @@ export function ruleFor(fromFile: string, toFile: string): string | null { } return null; } + if (to === 'programs' && from !== 'programs') { + const target = toFile.split(path.sep).join('/'); + if (target !== PROGRAMS_VALUES_ENTRY && target !== PROGRAMS_TYPES_ENTRY) { + return 'programs-deep-import'; + } + } return ALLOWED_IMPORTS[from].includes(to) ? null : `matrix:${from}->${to}`; } @@ -401,7 +415,12 @@ describe('surface classification', () => { expect(classifySurface('src/shared/utils/analytics.ts')).toBe('shared'); expect(classifySurface('src/shared/errors/codes.ts')).toBe('shared'); expect(classifySurface('src/agent/agent-runner.ts')).toBe('agent'); + expect(classifySurface('src/programs/program-registry.ts')).toBe( + 'programs', + ); expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); + expect(classifySurface('src/ui/index.ts')).toBe('tui'); + expect(classifySurface('src/steps/index.ts')).toBe('tui'); expect(classifySurface('bin.ts')).toBe('cli'); expect(classifySurface('src/agent/tools/mcp.ts')).toBe('agent'); expect(classifySurface('src/agent/tools/tools.ts')).toBe('agent'); @@ -409,32 +428,32 @@ describe('surface classification', () => { 'tui', ); expect( - classifySurface('src/lib/programs/posthog-integration/content/index.tsx'), + classifySurface('src/ui/tui/decks/posthog-integration/index.tsx'), ).toBe('tui'); - expect( - classifySurface('src/lib/programs/posthog-integration/index.ts'), - ).toBe('legacy'); + expect(classifySurface('src/programs/posthog-integration/index.ts')).toBe( + 'programs', + ); }); }); describe('agent entry modules', () => { const rule = (from: string, to: string) => ruleFor(from, to); - it('lets legacy and cli code reach the agent through its entries only', () => { - expect(rule('src/lib/programs/audit/index.ts', 'src/agent/index.ts')).toBe( + it('lets programs and cli code reach the agent through its entries only', () => { + expect(rule('src/programs/audit/index.ts', 'src/agent/index.ts')).toBe( null, ); - expect(rule('src/lib/programs/audit/index.ts', 'src/agent/types.ts')).toBe( + expect(rule('src/programs/audit/index.ts', 'src/agent/types.ts')).toBe( null, ); expect(rule('src/commands/skill.ts', 'src/agent/index.ts')).toBe(null); expect( - rule('src/lib/programs/audit/index.ts', 'src/agent/agent-runner.ts'), + rule('src/programs/audit/index.ts', 'src/agent/agent-runner.ts'), ).toBe('agent-deep-import'); expect(rule('src/commands/skill.ts', 'src/agent/runner/index.ts')).toBe( 'agent-deep-import', ); - expect(rule('src/shared/errors/agent-map.ts', 'src/agent/signals.ts')).toBe( + expect(rule('src/shared/claude-settings.ts', 'src/agent/signals.ts')).toBe( 'agent-deep-import', ); }); @@ -453,11 +472,32 @@ describe('agent entry modules', () => { expect(rule('src/agent/runner/index.ts', 'src/agent/progress.ts')).toBe( null, ); - expect(rule('src/lib/programs/audit/index.ts', 'src/ui/tui/store.ts')).toBe( - 'matrix:legacy->tui', + expect(rule('src/programs/audit/index.ts', 'src/ui/tui/store.ts')).toBe( + 'matrix:programs->tui', ); expect(rule('src/agent/runner/index.ts', 'src/ui/tui/store.ts')).toBe( 'matrix:agent->tui', ); }); }); + +describe('programs entry modules', () => { + const rule = (from: string, to: string) => ruleFor(from, to); + + it('lets hosts reach programs through its entries', () => { + expect(rule('src/commands/audit.ts', 'src/programs/index.ts')).toBe(null); + expect(rule('src/ui/tui/store.ts', 'src/programs/types.ts')).toBe(null); + expect(rule('src/commands/audit.ts', 'src/programs/audit/index.ts')).toBe( + 'programs-deep-import', + ); + }); + + it('keeps programs from reaching the TUI and CLI', () => { + expect(rule('src/programs/audit/index.ts', 'src/ui/tui/store.ts')).toBe( + 'matrix:programs->tui', + ); + expect( + rule('src/programs/dispatch-family.ts', 'src/commands/command.ts'), + ).toBe('matrix:programs->cli'); + }); +}); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 629085454..9273dfd7f 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -1,42 +1,173 @@ { "violations": [ + "src/agent/runner/switchboard/flags/index.ts -> src/programs/types.ts", + "src/agent/runner/switchboard/flags/schemes.ts -> src/programs/types.ts", + "src/agent/runner/switchboard/index.ts -> src/programs/types.ts", + "src/commands/ai-observability.ts -> src/programs/ai-observability/index.ts", + "src/commands/audit.ts -> src/programs/audit/index.ts", + "src/commands/basic-integration/ci-install.ts -> src/programs/posthog-integration/index.ts", + "src/commands/basic-integration/interactive.ts -> src/programs/posthog-integration/index.ts", + "src/commands/basic-integration/skill.ts -> src/programs/agent-skill/index.ts", + "src/commands/doctor.ts -> src/programs/posthog-doctor/index.ts", + "src/commands/error-tracking.ts -> src/programs/error-tracking/index.ts", + "src/commands/factories/family-command-factory.ts -> src/programs/dispatch-family.ts", "src/commands/factories/family-picker.tsx -> src/commands/command.ts", + "src/commands/mcp-analytics.ts -> src/programs/mcp-analytics/index.ts", + "src/commands/metrics.ts -> src/programs/metrics/index.ts", + "src/commands/migrate.ts -> src/programs/migration/index.ts", + "src/commands/replay-vision.ts -> src/programs/replay-vision/index.ts", + "src/commands/revenue.ts -> src/programs/revenue-analytics/index.ts", + "src/commands/self-driving.ts -> src/programs/self-driving/index.ts", + "src/commands/upload-sourcemaps.ts -> src/programs/error-tracking-upload-source-maps/index.ts", + "src/commands/warehouse.ts -> src/programs/warehouse-source/index.ts", "src/env.ts -> src/lib/headless-mode.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/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/index.ts -> src/lib/programs/error-tracking-upload-source-maps/content/index.tsx", - "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/metrics/index.ts -> src/lib/programs/agent-skill/content/index.tsx", - "src/lib/programs/migration/index.ts -> src/lib/programs/migration/content/index.tsx", - "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/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/revenue-analytics/index.ts -> src/lib/programs/revenue-analytics/content/index.tsx", - "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/warehouse-source/index.ts -> src/lib/programs/warehouse-source/content/index.tsx", - "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/runners/run-non-interactive.ts -> src/programs/audit/types.ts", + "src/lib/runners/run-non-interactive.ts -> src/programs/detect-map.ts", + "src/lib/runners/run-non-interactive.ts -> src/programs/run-agent-legacy.ts", + "src/lib/runners/run-non-interactive.ts -> src/programs/task-stream/index.ts", + "src/lib/runners/run-non-interactive.ts -> src/programs/task-stream/task-stream-push.ts", + "src/lib/runners/run-wizard.ts -> src/programs/audit/types.ts", + "src/lib/runners/run-wizard.ts -> src/programs/authenticate.ts", + "src/lib/runners/run-wizard.ts -> src/programs/posthog-integration/detect.ts", + "src/lib/runners/run-wizard.ts -> src/programs/run-agent-legacy.ts", + "src/lib/runners/run-wizard.ts -> src/programs/task-stream/destinations/file.ts", + "src/lib/runners/run-wizard.ts -> src/programs/task-stream/destinations/posthog.ts", + "src/lib/runners/run-wizard.ts -> src/programs/task-stream/index.ts", + "src/lib/runners/run-wizard.ts -> src/programs/task-stream/task-stream-push.ts", "src/lib/wizard-session.ts -> src/agent/progress.ts", - "src/shared/claude-settings.ts -> src/agent/agent-env-isolation.ts", - "src/shared/errors/agent-map.ts -> src/agent/signals.ts", + "src/lib/wizard-session.ts -> src/programs/framework-config.ts", + "src/lib/wizard-spellbook.ts -> src/programs/program-step.ts", + "src/programs/agent-skill/index.ts -> src/ui/tui/decks/agent-skill/index.tsx", + "src/programs/agent-skill/steps.ts -> src/lib/wizard-session.ts", + "src/programs/ai-observability/index.ts -> src/lib/headless-mode.ts", + "src/programs/ai-observability/index.ts -> src/ui/tui/decks/agent-skill/index.tsx", + "src/programs/ai-opt-in-gate.ts -> src/lib/wizard-session.ts", + "src/programs/audit/index.ts -> src/lib/headless-mode.ts", + "src/programs/audit/index.ts -> src/lib/wizard-session.ts", + "src/programs/audit/ledger-watcher.ts -> src/lib/file-watcher.ts", + "src/programs/audit/ledger-watcher.ts -> src/ui/index.ts", + "src/programs/audit/types.ts -> src/lib/wizard-session.ts", + "src/programs/authenticate.ts -> src/lib/wizard-session.ts", + "src/programs/authenticate.ts -> src/ui/index.ts", + "src/programs/detection/agentic.ts -> src/lib/wizard-session.ts", + "src/programs/detection/agentic.ts -> src/ui/agent-progress.ts", + "src/programs/detection/agentic.ts -> src/ui/index.ts", + "src/programs/detection/features.ts -> src/lib/wizard-session.ts", + "src/programs/detection/project-scope.ts -> src/lib/wizard-session.ts", + "src/programs/detection/project-scope.ts -> src/ui/index.ts", + "src/programs/dispatch-family.ts -> src/commands/command.ts", + "src/programs/dispatch-family.ts -> src/commands/factories/shared.ts", + "src/programs/error-tracking-upload-source-maps/detect-agentic.ts -> src/lib/wizard-session.ts", + "src/programs/error-tracking-upload-source-maps/detect.ts -> src/lib/wizard-session.ts", + "src/programs/error-tracking-upload-source-maps/index.ts -> src/lib/wizard-session.ts", + "src/programs/error-tracking-upload-source-maps/index.ts -> src/ui/index.ts", + "src/programs/error-tracking-upload-source-maps/index.ts -> src/ui/tui/decks/error-tracking-upload-source-maps/index.tsx", + "src/programs/error-tracking-upload-source-maps/steps.ts -> src/lib/wizard-session.ts", + "src/programs/error-tracking/detect-agentic.ts -> src/lib/wizard-session.ts", + "src/programs/error-tracking/index.ts -> src/lib/wizard-session.ts", + "src/programs/error-tracking/index.ts -> src/ui/tui/decks/error-tracking/index.tsx", + "src/programs/error-tracking/index.ts -> src/ui/tui/decks/error-tracking/tips.ts", + "src/programs/events-audit/index.ts -> src/lib/wizard-session.ts", + "src/programs/events-audit/steps.ts -> src/lib/wizard-session.ts", + "src/programs/frameworks/astro/astro-wizard-agent.ts -> src/ui/index.ts", + "src/programs/frameworks/django/utils.ts -> src/ui/index.ts", + "src/programs/frameworks/fastapi/utils.ts -> src/ui/index.ts", + "src/programs/frameworks/flask/utils.ts -> src/ui/index.ts", + "src/programs/frameworks/laravel/utils.ts -> src/ui/index.ts", + "src/programs/frameworks/nextjs/nextjs-wizard-agent.ts -> src/ui/index.ts", + "src/programs/frameworks/rails/utils.ts -> src/ui/index.ts", + "src/programs/frameworks/react-native/utils.ts -> src/ui/index.ts", + "src/programs/frameworks/react-router/react-router-wizard-agent.ts -> src/ui/index.ts", + "src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts -> src/ui/index.ts", + "src/programs/mcp/index.ts -> src/lib/wizard-session.ts", + "src/programs/metrics/index.ts -> src/ui/tui/decks/agent-skill/index.tsx", + "src/programs/migration/index.ts -> src/ui/tui/decks/migration/index.tsx", + "src/programs/migration/steps.ts -> src/lib/wizard-session.ts", + "src/programs/posthog-integration/detect.ts -> src/lib/wizard-session.ts", + "src/programs/posthog-integration/index.ts -> src/lib/wizard-session.ts", + "src/programs/posthog-integration/index.ts -> src/steps/index.ts", + "src/programs/posthog-integration/index.ts -> src/ui/index.ts", + "src/programs/posthog-integration/index.ts -> src/ui/tui/decks/posthog-integration/index.tsx", + "src/programs/posthog-integration/steps.ts -> src/lib/wizard-session.ts", + "src/programs/program-registry.ts -> src/ui/tui/decks/agent-skill/index.tsx", + "src/programs/program-run.ts -> src/lib/wizard-session.ts", + "src/programs/program-step.ts -> src/lib/wizard-session.ts", + "src/programs/program-step.ts -> src/ui/tui/components/TipsCard.tsx", + "src/programs/program-step.ts -> src/ui/tui/primitives/index.ts", + "src/programs/program-step.ts -> src/ui/tui/store.ts", + "src/programs/replay-vision/index.ts -> src/lib/wizard-session.ts", + "src/programs/revenue-analytics/detect.ts -> src/lib/wizard-session.ts", + "src/programs/revenue-analytics/index.ts -> src/ui/tui/decks/revenue-analytics/index.tsx", + "src/programs/revenue-analytics/steps.ts -> src/lib/wizard-session.ts", + "src/programs/run-agent-legacy.ts -> src/lib/wizard-session.ts", + "src/programs/run-agent-legacy.ts -> src/ui/agent-progress.ts", + "src/programs/run-agent-legacy.ts -> src/ui/index.ts", + "src/programs/self-driving/detect-agentic.ts -> src/lib/wizard-session.ts", + "src/programs/self-driving/detect.ts -> src/lib/wizard-session.ts", + "src/programs/self-driving/index.ts -> src/lib/wizard-session.ts", + "src/programs/self-driving/index.ts -> src/ui/tui/decks/self-driving/index.tsx", + "src/programs/self-driving/index.ts -> src/ui/tui/decks/self-driving/pricing.ts", + "src/programs/self-driving/index.ts -> src/ui/tui/decks/self-driving/tips.ts", + "src/programs/self-driving/steps.ts -> src/lib/wizard-session.ts", + "src/programs/shared/health-check-step.ts -> src/lib/wizard-session.ts", + "src/programs/shared/posthog-cli-preinstall.ts -> src/steps/install-cli-steering/index.ts", + "src/programs/shared/posthog-cli-preinstall.ts -> src/ui/index.ts", + "src/programs/task-stream/destinations/posthog.ts -> src/lib/wizard-session.ts", + "src/programs/task-stream/event-plan-watcher.ts -> src/lib/file-watcher.ts", + "src/programs/task-stream/event-plan-watcher.ts -> src/ui/tui/store.ts", + "src/programs/task-stream/task-stream-push.ts -> src/lib/wizard-session.ts", + "src/programs/task-stream/task-stream-push.ts -> src/ui/tui/store.ts", + "src/programs/task-stream/task-stream-push.ts -> src/ui/wizard-ui.ts", + "src/programs/task-stream/types.ts -> src/lib/wizard-session.ts", + "src/programs/warehouse-source/detect.ts -> src/lib/wizard-session.ts", + "src/programs/warehouse-source/index.ts -> src/lib/wizard-session.ts", + "src/programs/warehouse-source/index.ts -> src/ui/tui/decks/warehouse-source/index.tsx", + "src/programs/warehouse-source/steps.ts -> src/lib/wizard-session.ts", + "src/programs/web-analytics-doctor/detect.ts -> src/lib/wizard-session.ts", "src/shared/utils/analytics.ts -> src/lib/wizard-session.ts", "src/shared/utils/oauth.ts -> src/ui/index.ts", - "src/shared/utils/setup-utils.ts -> src/lib/oauth/program-scopes.ts", - "src/shared/utils/setup-utils.ts -> src/lib/programs/program-registry.ts", "src/shared/utils/setup-utils.ts -> src/lib/wizard-session.ts", + "src/shared/utils/setup-utils.ts -> src/programs/oauth/program-scopes.ts", + "src/shared/utils/setup-utils.ts -> src/programs/types.ts", "src/shared/utils/setup-utils.ts -> src/ui/index.ts", "src/shared/utils/wizard-abort.ts -> src/lib/wizard-session.ts", "src/shared/utils/wizard-abort.ts -> src/ui/index.ts", "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts", - "src/ui/index.ts -> src/ui/logging-ui.ts", + "src/ui/tui/decks/error-tracking/tips.ts -> src/programs/replay-vision/index.ts", + "src/ui/tui/playground/demos/AuditChecksDemo.tsx -> src/programs/audit/types.ts", + "src/ui/tui/playground/demos/DoctorReportDemo.tsx -> src/programs/posthog-doctor/index.ts", + "src/ui/tui/screen-sequences.ts -> src/programs/ai-opt-in-gate.ts", + "src/ui/tui/screen-sequences.ts -> src/programs/program-step.ts", + "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx -> src/programs/error-tracking/detect-agentic.ts", + "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx -> src/programs/registry.ts", + "src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx -> src/programs/registry.ts", + "src/ui/tui/screens/RevenueIntroScreen.tsx -> src/programs/revenue-analytics/index.ts", + "src/ui/tui/screens/SelfDrivingGitHubScreen.tsx -> src/programs/self-driving/detect.ts", + "src/ui/tui/screens/SelfDrivingHandoffScreen.tsx -> src/programs/posthog-integration/index.ts", + "src/ui/tui/screens/SelfDrivingHandoffScreen.tsx -> src/programs/self-driving/detect.ts", + "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/registry.ts", + "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/self-driving/detect-agentic.ts", + "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/self-driving/detect.ts", + "src/ui/tui/screens/SelfDrivingIntroScreen.tsx -> src/programs/self-driving/index.ts", + "src/ui/tui/screens/SourceMapsDetectScreen.tsx -> src/programs/error-tracking-upload-source-maps/detect-agentic.ts", + "src/ui/tui/screens/SourceMapsDetectScreen.tsx -> src/programs/error-tracking-upload-source-maps/index.ts", + "src/ui/tui/screens/WarehouseIntroScreen.tsx -> src/programs/warehouse-source/index.ts", + "src/ui/tui/screens/audit/AuditAreaPane.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksOutroSection.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/sort.ts -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditOutroScreen.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/AuditRunScreen.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/audit/PendingChecksList.tsx -> src/programs/audit/types.ts", + "src/ui/tui/screens/doctor/DoctorReportScreen.tsx -> src/programs/posthog-doctor/index.ts", + "src/ui/tui/screens/doctor/IssueTable.tsx -> src/programs/posthog-doctor/index.ts", "src/ui/tui/screens/health/HealthCheckScreen.tsx -> src/agent/index.ts", - "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/index.ts" + "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/agent/index.ts", + "src/ui/tui/store.ts -> src/programs/ai-opt-in-gate.ts", + "src/ui/tui/store.ts -> src/programs/posthog-integration/detect.ts" ] } diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 73de7fa8a..c740d3fc8 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -18,7 +18,7 @@ const { mockStreamAttach, mockStreamShutdown, mockStreamDestinations } = // destinations, not about whether a stream exists. mockStreamDestinations: vi.fn(), })); -vi.mock('../lib/task-stream/index', () => ({ +vi.mock('../programs/task-stream', () => ({ // shutdown() hardcodes a resolved Promise (not a bare vi.fn) so the // interactive runWizard's dangling SIGTERM handler — which calls // shutdown().catch() and outlives these tests — never hits undefined.catch. @@ -75,7 +75,7 @@ vi.mock('../ui/tui/start-tui', () => ({ }, }), })); -vi.mock('../lib/programs/posthog-integration/index', () => ({ +vi.mock('../programs/posthog-integration', () => ({ posthogIntegrationConfig: { id: 'posthog-integration', steps: [], @@ -101,8 +101,8 @@ vi.mock('@utils/debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn(), })); -vi.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); -vi.mock('../lib/detection/index', () => ({ +vi.mock('../programs/registry', () => ({ FRAMEWORK_REGISTRY: {} })); +vi.mock('../programs/detection', () => ({ detectFramework: vi.fn().mockResolvedValue(null), gatherFrameworkContext: vi.fn().mockResolvedValue({}), })); @@ -113,7 +113,7 @@ vi.mock('@utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../lib/programs/run-agent-legacy', () => ({ +vi.mock('../programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/__tests__/mcp-cli.test.ts b/src/__tests__/mcp-cli.test.ts index e9b41f8b3..98b7afa8a 100644 --- a/src/__tests__/mcp-cli.test.ts +++ b/src/__tests__/mcp-cli.test.ts @@ -25,7 +25,7 @@ vi.mock('@ui/tui/start-tui', () => ({ vi.mock('@utils/env-api-key', () => ({ readApiKeyFromEnv: mockReadApiKeyFromEnvMcp, })); -vi.mock('@lib/programs/program-registry', () => ({ +vi.mock('@programs', () => ({ Program: { McpAdd: 'mcp-add', McpRemove: 'mcp-remove', diff --git a/src/__tests__/programs-cli.test.ts b/src/__tests__/programs-cli.test.ts index c7a5e074e..998808992 100644 --- a/src/__tests__/programs-cli.test.ts +++ b/src/__tests__/programs-cli.test.ts @@ -29,11 +29,11 @@ import { selfDrivingCommand } from '../commands/self-driving'; import { dispatchFamily, pickerChildrenToShow, -} from '@lib/programs/dispatch-family'; +} from '@programs/dispatch-family'; import type { Command } from '../commands/command'; import { fetchSkillMenu, type CliEntry } from '@shared/skill-menu'; -import { auditConfig } from '@lib/programs/audit/index'; -import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; +import { auditConfig } from '@programs/audit/index'; +import { webAnalyticsDoctorConfig } from '@programs/web-analytics-doctor/index'; import { parseCommand } from './helpers/parse-command.no-jest'; const mockFetchSkillMenu = fetchSkillMenu as MockedFunction< diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 94b76cb25..0224d25fa 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -27,7 +27,7 @@ vi.mock('../ui/tui/start-tui', () => ({ }, }), })); -vi.mock('../lib/programs/posthog-integration/index', () => ({ +vi.mock('../programs/posthog-integration', () => ({ posthogIntegrationConfig: { id: 'posthog-integration', steps: [], @@ -52,8 +52,8 @@ vi.mock('@utils/debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn(), })); -vi.mock('../lib/registry', () => ({ FRAMEWORK_REGISTRY: {} })); -vi.mock('../lib/detection/index', () => ({ +vi.mock('../programs/registry', () => ({ FRAMEWORK_REGISTRY: {} })); +vi.mock('../programs/detection', () => ({ detectFramework: vi.fn().mockResolvedValue(null), gatherFrameworkContext: vi.fn().mockResolvedValue({}), })); @@ -64,7 +64,7 @@ vi.mock('@utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../lib/programs/run-agent-legacy', () => ({ +vi.mock('../programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/agent/README.md b/src/agent/README.md index 1c86b2e08..ee572fea5 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -45,7 +45,7 @@ if (result.outcome !== RunOutcome.Success) { ## Intent -Programs call the agent to do the work a skill describes. The TUI and the headless runner observe the run through `onProgress` and answer it through `interaction`; today `src/lib/programs/run-agent-legacy.ts` does both on top of the session. +Programs call the agent to do the work a skill describes. The TUI and the headless runner observe the run through `onProgress` and answer it through `interaction`; today `src/programs/run-agent-legacy.ts` does both on top of the session. Without `onProgress` the run completes and its snapshot still comes back in the result. Without `interaction` the agent installs no ask bridge: `wizard_ask` returns its "not available" error and optional task notices are declined, which is what a `--ci` run does. A throwing observer is logged and the run continues. diff --git a/src/agent/__tests__/credential-isolation-snapshot.test.ts b/src/agent/__tests__/credential-isolation-snapshot.test.ts index 40649ebcb..7fbe8be98 100644 --- a/src/agent/__tests__/credential-isolation-snapshot.test.ts +++ b/src/agent/__tests__/credential-isolation-snapshot.test.ts @@ -13,7 +13,7 @@ * isolation surface — a diff here means a credential path changed disposition. */ -import { sanitizeAgentSubprocessEnv } from '@agent/agent-env-isolation'; +import { sanitizeAgentSubprocessEnv } from '@shared/agent-env-isolation'; import { classifySettingsConflicts } from '@shared/claude-settings'; import type { SettingsConflict } from '@shared/claude-settings'; diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index d03bd9890..29c4956d7 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -16,7 +16,8 @@ import { type AskAnswers, type PendingQuestion, } from '@lib/wizard-session'; -import { AGENT_ERROR_CODE, ErrorCodes } from '@shared/errors'; +import { ErrorCodes } from '@shared/errors'; +import { AGENT_ERROR_CODE } from '@agent/error-map'; import { AgentErrorType } from '@agent/signals'; import type { AgentFailure } from '@agent/runner/shared/types'; import type { AgentProgress } from '@agent/progress'; diff --git a/src/agent/__tests__/wizard-tools.test.ts b/src/agent/__tests__/wizard-tools.test.ts index d61a953b1..65f884444 100644 --- a/src/agent/__tests__/wizard-tools.test.ts +++ b/src/agent/__tests__/wizard-tools.test.ts @@ -28,7 +28,7 @@ import { resolveEnvPath, templateEnvWriteRefusal, } from '@agent/tools'; -import type { AuditCheck } from '@lib/programs/audit/types'; +import type { AuditCheck } from '@programs/audit/types'; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'wizard-tools-')); diff --git a/src/agent/agent-interface.ts b/src/agent/agent-interface.ts index c418e5c19..9b73013c0 100644 --- a/src/agent/agent-interface.ts +++ b/src/agent/agent-interface.ts @@ -75,7 +75,7 @@ import { claudeConfigDir, createIsolatedAgentConfigDir, } from './stored-login'; -import { sanitizeAgentSubprocessEnv } from './agent-env-isolation'; +import { sanitizeAgentSubprocessEnv } from '@shared/agent-env-isolation'; // Dynamic import cache for ESM module let _sdkModule: any = null; diff --git a/src/agent/agent-runner.ts b/src/agent/agent-runner.ts index 7ffac5145..06a907fab 100644 --- a/src/agent/agent-runner.ts +++ b/src/agent/agent-runner.ts @@ -2,7 +2,7 @@ * Re-export shim. The runner has been split into agent/runner/. * Import from there directly; this shim keeps existing importers working. * The session-driven `runProgramAgent(programConfig, session)` lives in - * `src/lib/programs/run-agent-legacy.ts`. + * `src/programs/run-agent-legacy.ts`. */ export { diff --git a/src/shared/errors/agent-map.ts b/src/agent/error-map.ts similarity index 62% rename from src/shared/errors/agent-map.ts rename to src/agent/error-map.ts index e897b5085..6e3599787 100644 --- a/src/shared/errors/agent-map.ts +++ b/src/agent/error-map.ts @@ -1,8 +1,5 @@ -// Leaf module on purpose: every module imports the errors, so the agent entry -// would form a module cycle here. -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B1: error codes move next to their owners -import { AgentErrorType } from '@agent/signals'; -import { ErrorCodes, type ErrorCode } from './codes'; +import { AgentErrorType } from './signals'; +import { ErrorCodes, type ErrorCode } from '@shared/errors'; export const AGENT_ERROR_CODE: Record = { [AgentErrorType.MCP_MISSING]: ErrorCodes.AgentMcpMissing, diff --git a/src/agent/mcp-prompt-streaming.ts b/src/agent/mcp-prompt-streaming.ts index 6bafc82fb..2c1586550 100644 --- a/src/agent/mcp-prompt-streaming.ts +++ b/src/agent/mcp-prompt-streaming.ts @@ -17,7 +17,7 @@ import { DEFAULT_AGENT_MODEL, WIZARD_USER_AGENT } from '@shared/constants'; import { logToFile } from '@utils/debug'; import { gatewayAuth } from '@agent/gateway-session'; import { buildAgentEnv, buildRunTags } from '@agent/agent-interface'; -import { sanitizeAgentSubprocessEnv } from '@agent/agent-env-isolation'; +import { sanitizeAgentSubprocessEnv } from '@shared/agent-env-isolation'; import { createIsolatedAgentConfigDir } from '@agent/stored-login'; import { analytics } from '@utils/analytics'; diff --git a/src/agent/progress.ts b/src/agent/progress.ts index fe4858faf..97b11c428 100644 --- a/src/agent/progress.ts +++ b/src/agent/progress.ts @@ -4,7 +4,7 @@ * `runAgent` reports through one optional callback and asks through one * optional set of capabilities. Neither reaches into a UI singleton, a store, * or a session: every payload is copied data, every question is awaited on an - * injected answerer. The legacy adapter in `src/lib/programs/run-agent-legacy.ts` + * injected answerer. The legacy adapter in `src/programs/run-agent-legacy.ts` * maps these back onto `WizardUI` one call per event, so the terminal output of * every existing runner is unchanged. */ diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index 79a87bfad..44dcbaa83 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -45,7 +45,7 @@ resolved execution data and an invocation snapshot (`shared/types.ts`), reports through `onProgress` and asks through `interaction` (`../progress.ts`), and returns every ending as a result. It never renders, reads a session or exits. The gates, OAuth, flags and binding lookup that used to run here live in -`src/lib/programs/run-agent-legacy.ts`, which also maps progress back onto +`src/programs/run-agent-legacy.ts`, which also maps progress back onto `getUI()` for today's runners. **Prepare** (`shared/bootstrap.ts`) is the on-ramp inside the agent: logging diff --git a/src/agent/runner/__tests__/switchboard.test.ts b/src/agent/runner/__tests__/switchboard.test.ts index 6b4f0e8fd..0a16a2048 100644 --- a/src/agent/runner/__tests__/switchboard.test.ts +++ b/src/agent/runner/__tests__/switchboard.test.ts @@ -10,7 +10,7 @@ * asserted directly. */ import { describe, it, expect } from 'vitest'; -import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; +import { PROGRAM_REGISTRY } from '@programs'; import { DEFAULT_AGENT_MODEL, GPT5_6_LUNA_MODEL, diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index 9f583e16c..8aee0984e 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -18,7 +18,7 @@ * agent did not decide (a refused mint, an SDK crash) comes back as * `outcome: RunOutcome.Crashed` with the original error attached, so a caller can keep * handling it the way it always did. The legacy adapter in - * `src/lib/programs/run-agent-legacy.ts` rebuilds today's session-driven + * `src/programs/run-agent-legacy.ts` rebuilds today's session-driven * behavior on top of this call for every existing caller. */ diff --git a/src/agent/runner/sequence/linear.ts b/src/agent/runner/sequence/linear.ts index e86ccbf4d..1e97adde7 100644 --- a/src/agent/runner/sequence/linear.ts +++ b/src/agent/runner/sequence/linear.ts @@ -14,7 +14,8 @@ import { OutroKind, type OutroData } from '@agent/progress'; import { AgentErrorType, AgentSignals } from '../../agent-interface'; import { logToFile } from '@utils/debug'; import { createBenchmarkPipeline } from '@agent/middleware/benchmark'; -import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@shared/errors'; +import { ErrorCodes, WizardError } from '@shared/errors'; +import { AGENT_ERROR_CODE } from '@agent/error-map'; import { analytics } from '@utils/analytics'; import { formatYaraAbortMessage } from '@agent/yara-hooks'; import { installSkillById } from '@agent/tools'; diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index 3f97d5f5d..0f79bc22c 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -5,7 +5,7 @@ * invocation snapshot, reports through `options.onProgress`, asks through * `options.interaction`, and returns a `RunResult`. Nothing here names a UI, * a store, a session or a program registry: the caller resolves those and - * hands over plain data. `src/lib/programs/run-agent-legacy.ts` is the caller + * hands over plain data. `src/programs/run-agent-legacy.ts` is the caller * that rebuilds today's session-driven behavior on top of this contract. */ @@ -42,7 +42,7 @@ export interface AbortCase { * * Every program provides one of these as `RunConfig.run`. The runner assembles * the final prompt from `customPrompt` + `skillId`. Programs extend it with - * their session-taking completion hooks in `src/lib/programs/program-run.ts`; + * their session-taking completion hooks in `src/programs/program-run.ts`; * the caller binds those and hands the agent `RunConfig.hooks` instead. */ export interface AgentRunDefinition { diff --git a/src/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/agent/runner/switchboard/flags/__tests__/flags.test.ts index 616ed2135..8c04f0983 100644 --- a/src/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { join, dirname } from 'node:path'; import { describe, it, expect, vi } from 'vitest'; -import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; +import { PROGRAM_REGISTRY } from '@programs'; import * as constants from '@shared/constants'; import { DEFAULT_AGENT_MODEL, diff --git a/src/agent/runner/switchboard/flags/index.ts b/src/agent/runner/switchboard/flags/index.ts index 5b376d3ef..23a03503f 100644 --- a/src/agent/runner/switchboard/flags/index.ts +++ b/src/agent/runner/switchboard/flags/index.ts @@ -7,7 +7,7 @@ import { RUN_SURFACE } from '@env'; import { logToFile } from '@utils/debug'; import type { Sequence } from '@shared/constants'; -import type { ProgramId } from '@lib/programs/program-registry'; +import type { ProgramId } from '@programs/types'; import { ORCHESTRATOR_HARNESS_ROUTE, ORCHESTRATOR_SEQUENCE_ROUTE, diff --git a/src/agent/runner/switchboard/flags/schemes.ts b/src/agent/runner/switchboard/flags/schemes.ts index e30f48c0e..99f9c4a97 100644 --- a/src/agent/runner/switchboard/flags/schemes.ts +++ b/src/agent/runner/switchboard/flags/schemes.ts @@ -13,7 +13,7 @@ import { Sequence, SONNET_5_MODEL, } from '@shared/constants'; -import type { ProgramId } from '@lib/programs/program-registry'; +import type { ProgramId } from '@programs/types'; import { logToFile } from '@utils/debug'; import type { EffortLevel } from '../models'; diff --git a/src/agent/runner/switchboard/index.ts b/src/agent/runner/switchboard/index.ts index 842176c16..b2d84add2 100644 --- a/src/agent/runner/switchboard/index.ts +++ b/src/agent/runner/switchboard/index.ts @@ -7,7 +7,7 @@ import { Harness, Sequence, } from '@shared/constants'; -import type { ProgramId } from '@lib/programs/program-registry'; +import type { ProgramId } from '@programs/types'; import { resolveHarness } from './harness'; import type { EffortLevel } from './models'; import { resolveSequence } from './sequence'; diff --git a/src/commands/ai-observability.ts b/src/commands/ai-observability.ts index abf436c45..c51fe2b0a 100644 --- a/src/commands/ai-observability.ts +++ b/src/commands/ai-observability.ts @@ -1,4 +1,4 @@ -import { aiObservabilityConfig } from '@lib/programs/ai-observability/index'; +import { aiObservabilityConfig } from '@programs/ai-observability/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/audit.ts b/src/commands/audit.ts index e838bbe28..39ed26dfe 100644 --- a/src/commands/audit.ts +++ b/src/commands/audit.ts @@ -1,4 +1,4 @@ -import { auditConfig } from '@lib/programs/audit/index'; +import { auditConfig } from '@programs/audit/index'; import type { Command } from './command'; import { familyCommandFactory } from './factories/family-command-factory'; diff --git a/src/commands/basic-integration/ci-install.ts b/src/commands/basic-integration/ci-install.ts index 9c24bc880..a1974e7a0 100644 --- a/src/commands/basic-integration/ci-install.ts +++ b/src/commands/basic-integration/ci-install.ts @@ -4,7 +4,7 @@ import { LoggingUI } from '@ui/logging-ui'; import { API_KEY_HINT, runWizardCI, runWizardHeadless } from '@lib/runners'; import type { NonInteractiveMode } from '@lib/runners'; import { provisionNewAccount } from '@utils/provisioning'; -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; +import { posthogIntegrationConfig } from '@programs/posthog-integration/index'; import { ErrorCodes, type ErrorCode } from '@shared/errors'; import { emitWizardError } from '@shared/errors'; diff --git a/src/commands/basic-integration/interactive.ts b/src/commands/basic-integration/interactive.ts index ee143f5d9..6191cf745 100644 --- a/src/commands/basic-integration/interactive.ts +++ b/src/commands/basic-integration/interactive.ts @@ -1,6 +1,6 @@ import type { Arguments } from 'yargs'; import { runWizard } from '@lib/runners'; -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; +import { posthogIntegrationConfig } from '@programs/posthog-integration/index'; /** Default flow: run the posthog-integration program through the TUI. */ export function runInteractive(argv: Arguments): void { diff --git a/src/commands/basic-integration/skill.ts b/src/commands/basic-integration/skill.ts index 5b41a205d..083f2dba2 100644 --- a/src/commands/basic-integration/skill.ts +++ b/src/commands/basic-integration/skill.ts @@ -1,7 +1,7 @@ import type { Arguments } from 'yargs'; import { POSTHOG_DOCS_URL } from '@shared/constants'; import { runWizard, runWizardCI } from '@lib/runners'; -import { createSkillProgram } from '@lib/programs/agent-skill/index'; +import { createSkillProgram } from '@programs/agent-skill/index'; /** Run an arbitrary context-mill skill by id (`wizard skill `, headless with `--ci`). */ export function runSkillMode(argv: Arguments): void { diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 3b9385b97..979c29116 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -8,7 +8,7 @@ import { posthogDoctorConfig, fetchHealthIssues, getKindMeta, -} from '@lib/programs/posthog-doctor/index'; +} from '@programs/posthog-doctor/index'; import { skillProgramOptions } from './skill-program-options'; import type { Command } from './command'; diff --git a/src/commands/error-tracking.ts b/src/commands/error-tracking.ts index 3db0b2816..962b1d612 100644 --- a/src/commands/error-tracking.ts +++ b/src/commands/error-tracking.ts @@ -1,4 +1,4 @@ -import { errorTrackingConfig } from '@lib/programs/error-tracking/index'; +import { errorTrackingConfig } from '@programs/error-tracking/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/factories/__tests__/native-command-factory.test.ts b/src/commands/factories/__tests__/native-command-factory.test.ts index 9539cc3bd..0cdc69e63 100644 --- a/src/commands/factories/__tests__/native-command-factory.test.ts +++ b/src/commands/factories/__tests__/native-command-factory.test.ts @@ -10,7 +10,7 @@ vi.mock('@lib/runners', () => ({ import type { Arguments } from 'yargs'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import { nativeCommandFactory } from '../native-command-factory'; diff --git a/src/commands/factories/family-command-factory.ts b/src/commands/factories/family-command-factory.ts index 087bd5d21..e26a38f55 100644 --- a/src/commands/factories/family-command-factory.ts +++ b/src/commands/factories/family-command-factory.ts @@ -1,11 +1,11 @@ import type { Arguments } from 'yargs'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import { buildFamilyPickerChildren, dispatchFamily, pickerChildrenToShow, -} from '@lib/programs/dispatch-family'; +} from '@programs/dispatch-family'; import { getSkillsBaseUrl } from '@shared/constants'; import { fetchSkillMenu } from '@shared/skill-menu'; diff --git a/src/commands/factories/native-command-factory.ts b/src/commands/factories/native-command-factory.ts index d9fd65a22..1cb4ae068 100644 --- a/src/commands/factories/native-command-factory.ts +++ b/src/commands/factories/native-command-factory.ts @@ -1,4 +1,4 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import type { Command } from '../command'; diff --git a/src/commands/factories/shared.ts b/src/commands/factories/shared.ts index cf2e73951..e38e96717 100644 --- a/src/commands/factories/shared.ts +++ b/src/commands/factories/shared.ts @@ -2,7 +2,7 @@ import type { Arguments, Options } from 'yargs'; import { runWizard, runWizardCI, runWizardHeadless } from '@lib/runners'; import { isHeadless } from '@lib/headless-mode'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import { skillProgramOptions } from '../skill-program-options'; import { ErrorCodes } from '@shared/errors'; diff --git a/src/commands/mcp-analytics.ts b/src/commands/mcp-analytics.ts index 6d49b114e..4b36a2f95 100644 --- a/src/commands/mcp-analytics.ts +++ b/src/commands/mcp-analytics.ts @@ -1,4 +1,4 @@ -import { mcpAnalyticsConfig } from '@lib/programs/mcp-analytics/index'; +import { mcpAnalyticsConfig } from '@programs/mcp-analytics/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/mcp/add.ts b/src/commands/mcp/add.ts index f9b22e7a0..887bc0bc1 100644 --- a/src/commands/mcp/add.ts +++ b/src/commands/mcp/add.ts @@ -2,7 +2,7 @@ import type { Arguments } from 'yargs'; import { setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { headlessOption, isHeadless } from '@lib/headless-mode'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { VERSION } from '@shared/version'; import type { Command } from '../command'; import { isTUIUnavailable } from './tui-availability'; diff --git a/src/commands/mcp/remove.ts b/src/commands/mcp/remove.ts index 26e34291e..9b82c728f 100644 --- a/src/commands/mcp/remove.ts +++ b/src/commands/mcp/remove.ts @@ -2,7 +2,7 @@ import type { Arguments } from 'yargs'; import { setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; import { headlessOption, isHeadless } from '@lib/headless-mode'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { VERSION } from '@shared/version'; import type { Command } from '../command'; import { isTUIUnavailable } from './tui-availability'; diff --git a/src/commands/mcp/tutorial.ts b/src/commands/mcp/tutorial.ts index eb6c0b495..6d2919142 100644 --- a/src/commands/mcp/tutorial.ts +++ b/src/commands/mcp/tutorial.ts @@ -1,7 +1,7 @@ import type { Arguments } from 'yargs'; import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { VERSION } from '@shared/version'; import { ErrorCodes } from '@shared/errors'; import { emitWizardError } from '@shared/errors'; diff --git a/src/commands/metrics.ts b/src/commands/metrics.ts index a889bd24c..0f0978b1a 100644 --- a/src/commands/metrics.ts +++ b/src/commands/metrics.ts @@ -1,4 +1,4 @@ -import { metricsConfig } from '@lib/programs/metrics/index'; +import { metricsConfig } from '@programs/metrics/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/migrate.ts b/src/commands/migrate.ts index e7389efcf..ec937197d 100644 --- a/src/commands/migrate.ts +++ b/src/commands/migrate.ts @@ -1,4 +1,4 @@ -import { migrationConfig } from '@lib/programs/migration/index'; +import { migrationConfig } from '@programs/migration/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/replay-vision.ts b/src/commands/replay-vision.ts index 10e8e2db9..8b13e19c2 100644 --- a/src/commands/replay-vision.ts +++ b/src/commands/replay-vision.ts @@ -1,4 +1,4 @@ -import { replayVisionConfig } from '@lib/programs/replay-vision/index'; +import { replayVisionConfig } from '@programs/replay-vision/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/revenue.ts b/src/commands/revenue.ts index a41f6af09..76f50c629 100644 --- a/src/commands/revenue.ts +++ b/src/commands/revenue.ts @@ -1,4 +1,4 @@ -import { revenueAnalyticsConfig } from '@lib/programs/revenue-analytics/index'; +import { revenueAnalyticsConfig } from '@programs/revenue-analytics/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/commands/self-driving.ts b/src/commands/self-driving.ts index 5f38f40e4..5a585e9b0 100644 --- a/src/commands/self-driving.ts +++ b/src/commands/self-driving.ts @@ -1,5 +1,5 @@ import { runWizard, runWizardCI } from '@lib/runners'; -import { selfDrivingConfig } from '@lib/programs/self-driving/index'; +import { selfDrivingConfig } from '@programs/self-driving/index'; import { skillProgramOptions } from './skill-program-options'; import type { Command } from './command'; diff --git a/src/commands/slack.ts b/src/commands/slack.ts index cd857e6c2..c65060356 100644 --- a/src/commands/slack.ts +++ b/src/commands/slack.ts @@ -1,7 +1,7 @@ import type { Arguments } from 'yargs'; import { getUI, setUI } from '@ui'; import { LoggingUI } from '@ui/logging-ui'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { VERSION } from '@shared/version'; import { ErrorCodes } from '@shared/errors'; import { emitWizardError } from '@shared/errors'; diff --git a/src/commands/upload-sourcemaps.ts b/src/commands/upload-sourcemaps.ts index 22e6f96c1..1ffc65079 100644 --- a/src/commands/upload-sourcemaps.ts +++ b/src/commands/upload-sourcemaps.ts @@ -1,5 +1,5 @@ import { runWizard, runWizardCI } from '@lib/runners'; -import { errorTrackingUploadSourceMapsConfig } from '@lib/programs/error-tracking-upload-source-maps/index'; +import { errorTrackingUploadSourceMapsConfig } from '@programs/error-tracking-upload-source-maps/index'; import { skillProgramOptions } from './skill-program-options'; import type { Command } from './command'; diff --git a/src/commands/warehouse.ts b/src/commands/warehouse.ts index b6c53e0df..1b9c52637 100644 --- a/src/commands/warehouse.ts +++ b/src/commands/warehouse.ts @@ -1,4 +1,4 @@ -import { warehouseSourceConfig } from '@lib/programs/warehouse-source/index'; +import { warehouseSourceConfig } from '@programs/warehouse-source/index'; import type { Command } from './command'; import { nativeCommandFactory } from './factories/native-command-factory'; diff --git a/src/lib/__tests__/wizard-spellbook.test.ts b/src/lib/__tests__/wizard-spellbook.test.ts index 2f15a66b7..0d0889c6f 100644 --- a/src/lib/__tests__/wizard-spellbook.test.ts +++ b/src/lib/__tests__/wizard-spellbook.test.ts @@ -2,7 +2,7 @@ import fs from 'fs/promises'; import os from 'os'; import path from 'path'; import { Integration } from '@shared/constants'; -import type { ProgramConfig } from '../programs/program-step'; +import type { ProgramConfig } from '../../programs/program-step'; import { buildSession } from '../wizard-session'; import { writeWizardSpellbook } from '../wizard-spellbook'; import { downloadSkill } from '@agent/tools/tools'; diff --git a/src/lib/registry.ts b/src/lib/registry.ts deleted file mode 100644 index 0c1ae0b1c..000000000 --- a/src/lib/registry.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { FrameworkConfig } from './framework-config'; -import { Integration } from '@shared/constants'; -import { NEXTJS_AGENT_CONFIG } from '@frameworks/nextjs/nextjs-wizard-agent'; -import { NUXT_AGENT_CONFIG } from '@frameworks/nuxt/nuxt-wizard-agent'; -import { VUE_AGENT_CONFIG } from '@frameworks/vue/vue-wizard-agent'; -import { REACT_ROUTER_AGENT_CONFIG } from '@frameworks/react-router/react-router-wizard-agent'; -import { TANSTACK_ROUTER_AGENT_CONFIG } from '@frameworks/tanstack-router/tanstack-router-wizard-agent'; -import { TANSTACK_START_AGENT_CONFIG } from '@frameworks/tanstack-start/tanstack-start-wizard-agent'; -import { REACT_NATIVE_AGENT_CONFIG } from '@frameworks/react-native/react-native-wizard-agent'; -import { ANGULAR_AGENT_CONFIG } from '@frameworks/angular/angular-wizard-agent'; -import { ASTRO_AGENT_CONFIG } from '@frameworks/astro/astro-wizard-agent'; -import { DJANGO_AGENT_CONFIG } from '@frameworks/django/django-wizard-agent'; -import { FLASK_AGENT_CONFIG } from '@frameworks/flask/flask-wizard-agent'; -import { FASTAPI_AGENT_CONFIG } from '@frameworks/fastapi/fastapi-wizard-agent'; -import { LARAVEL_AGENT_CONFIG } from '@frameworks/laravel/laravel-wizard-agent'; -import { SVELTEKIT_AGENT_CONFIG } from '@frameworks/svelte/svelte-wizard-agent'; -import { FLUTTER_AGENT_CONFIG } from '@frameworks/flutter/flutter-wizard-agent'; -import { SWIFT_AGENT_CONFIG } from '@frameworks/swift/swift-wizard-agent'; -import { KMP_AGENT_CONFIG } from '@frameworks/kmp/kmp-wizard-agent'; -import { ANDROID_AGENT_CONFIG } from '@frameworks/android/android-wizard-agent'; -import { RAILS_AGENT_CONFIG } from '@frameworks/rails/rails-wizard-agent'; -import { ELIXIR_AGENT_CONFIG } from '@frameworks/elixir/elixir-wizard-agent'; -import { GO_AGENT_CONFIG } from '@frameworks/go/go-wizard-agent'; -import { JAVA_AGENT_CONFIG } from '@frameworks/java/java-wizard-agent'; -import { RUST_AGENT_CONFIG } from '@frameworks/rust/rust-wizard-agent'; -import { PYTHON_AGENT_CONFIG } from '@frameworks/python/python-wizard-agent'; -import { RUBY_AGENT_CONFIG } from '@frameworks/ruby/ruby-wizard-agent'; -import { JAVASCRIPT_NODE_AGENT_CONFIG } from '@frameworks/javascript-node/javascript-node-wizard-agent'; -import { JAVASCRIPT_WEB_AGENT_CONFIG } from '@frameworks/javascript-web/javascript-web-wizard-agent'; - -export const FRAMEWORK_REGISTRY: Record = { - [Integration.nextjs]: NEXTJS_AGENT_CONFIG, - [Integration.nuxt]: NUXT_AGENT_CONFIG, - [Integration.vue]: VUE_AGENT_CONFIG, - [Integration.tanstackStart]: TANSTACK_START_AGENT_CONFIG, - [Integration.reactRouter]: REACT_ROUTER_AGENT_CONFIG, - [Integration.tanstackRouter]: TANSTACK_ROUTER_AGENT_CONFIG, - [Integration.reactNative]: REACT_NATIVE_AGENT_CONFIG, - [Integration.angular]: ANGULAR_AGENT_CONFIG, - [Integration.astro]: ASTRO_AGENT_CONFIG, - [Integration.django]: DJANGO_AGENT_CONFIG, - [Integration.flask]: FLASK_AGENT_CONFIG, - [Integration.fastapi]: FASTAPI_AGENT_CONFIG, - [Integration.laravel]: LARAVEL_AGENT_CONFIG, - [Integration.sveltekit]: SVELTEKIT_AGENT_CONFIG, - [Integration.flutter]: FLUTTER_AGENT_CONFIG, - [Integration.kmp]: KMP_AGENT_CONFIG, - [Integration.swift]: SWIFT_AGENT_CONFIG, - [Integration.android]: ANDROID_AGENT_CONFIG, - [Integration.rails]: RAILS_AGENT_CONFIG, - [Integration.elixir]: ELIXIR_AGENT_CONFIG, - [Integration.go]: GO_AGENT_CONFIG, - [Integration.rust]: RUST_AGENT_CONFIG, - [Integration.java]: JAVA_AGENT_CONFIG, - [Integration.python]: PYTHON_AGENT_CONFIG, - [Integration.ruby]: RUBY_AGENT_CONFIG, - [Integration.javascriptNode]: JAVASCRIPT_NODE_AGENT_CONFIG, - [Integration.javascript_web]: JAVASCRIPT_WEB_AGENT_CONFIG, -}; diff --git a/src/lib/runners/__tests__/mint-recovery.test.ts b/src/lib/runners/__tests__/mint-recovery.test.ts index affbebc70..ada5cf6c2 100644 --- a/src/lib/runners/__tests__/mint-recovery.test.ts +++ b/src/lib/runners/__tests__/mint-recovery.test.ts @@ -1,16 +1,16 @@ import { vi, it, expect, afterEach } from 'vitest'; import { runWizard } from '../run-wizard'; -import { runProgramAgent } from '@lib/programs/run-agent-legacy'; +import { runProgramAgent } from '@programs/run-agent-legacy'; import { startTUI } from '@ui/tui/start-tui'; import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui'; -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration'; +import { posthogIntegrationConfig } from '@programs/posthog-integration'; import { ScreenId } from '@ui/tui/router'; import { HostResolution } from '@shared/host-resolution'; import { analytics } from '@utils/analytics'; -vi.mock('@lib/programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn() })); +vi.mock('@programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn() })); vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); vi.mock('@shared/local-dev', async (original) => ({ ...(await original()), @@ -27,7 +27,7 @@ vi.mock('@utils/analytics', () => ({ }, sessionProperties: () => ({}), })); -vi.mock('@lib/task-stream/index', () => ({ +vi.mock('@programs/task-stream/index', () => ({ TaskStreamPush: class { attach = vi.fn(); shutdown() { @@ -35,7 +35,7 @@ vi.mock('@lib/task-stream/index', () => ({ } }, })); -vi.mock('@lib/task-stream/destinations/posthog', () => ({ +vi.mock('@programs/task-stream/destinations/posthog', () => ({ PostHogDestination: class {}, })); diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 6977ae5f9..308532649 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -11,19 +11,19 @@ import { 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 { getAuditChecks } from '@lib/programs/audit/types'; +import type { ProgramConfig } from '@programs/types'; +import { getAuditChecks } from '@programs/audit/types'; import { analytics } from '@utils/analytics'; import { resolveNoTelemetry } from './resolve-no-telemetry'; import type { WizardStore } from '@ui/tui/store'; -import type { TaskStreamPush } from '@lib/task-stream/task-stream-push'; +import type { TaskStreamPush } from '@programs/task-stream/task-stream-push'; import { join } from 'node:path'; import { ErrorCodes, classifyRunFailure, emitWizardError, } from '@shared/errors'; -import { detectErrorCode } from '@lib/programs/detect-map'; +import { detectErrorCode } from '@programs/detect-map'; import type { OutroData, RunPhase as RunPhaseT } from '@lib/wizard-session'; /** @@ -187,7 +187,7 @@ export function runNonInteractive( const { WizardStore } = await import('@ui/tui/store'); const { HeadlessUI } = await import('@ui/headless-ui'); const { TaskStreamPush, PostHogDestination, createFileDestination } = - await import('@lib/task-stream/index'); + await import('@programs/task-stream/index'); // `''` resolves to the default path, so `--ci` always dumps. const logTarget = @@ -337,9 +337,7 @@ export function runNonInteractive( } } - const { runProgramAgent } = await import( - '@lib/programs/run-agent-legacy' - ); + const { runProgramAgent } = await import('@programs/run-agent-legacy'); await runProgramAgent(config, session); await settleStream(RunPhase.Completed); } catch (error) { diff --git a/src/lib/runners/run-wizard-ci.ts b/src/lib/runners/run-wizard-ci.ts index e278bd5f5..247f558f9 100644 --- a/src/lib/runners/run-wizard-ci.ts +++ b/src/lib/runners/run-wizard-ci.ts @@ -1,4 +1,4 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import { runNonInteractive } from './run-non-interactive'; /** diff --git a/src/lib/runners/run-wizard-headless.ts b/src/lib/runners/run-wizard-headless.ts index 20916c8eb..c88744019 100644 --- a/src/lib/runners/run-wizard-headless.ts +++ b/src/lib/runners/run-wizard-headless.ts @@ -1,4 +1,4 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import { runNonInteractive } from './run-non-interactive'; /** diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index a3eaa8f5c..2ef2ff9cf 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,16 +1,16 @@ import { VERSION } from '@shared/version'; import { logToFile, getLogFilePath } from '@utils/debug'; -import { runProgramAgent } from '@lib/programs/run-agent-legacy'; -import { authenticate } from '@lib/programs/authenticate'; -import { getProgramConfig } from '@lib/programs/program-registry'; -import { getAuditChecks } from '@lib/programs/audit/types'; -import { maybeStampAiSdkDetected } from '@lib/programs/posthog-integration/detect'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import { runProgramAgent } from '@programs/run-agent-legacy'; +import { authenticate } from '@programs/authenticate'; +import { getProgramConfig } from '@programs'; +import { getAuditChecks } from '@programs/audit/types'; +import { maybeStampAiSdkDetected } from '@programs/posthog-integration/detect'; +import type { ProgramConfig } from '@programs/types'; import type { Harness, Sequence } from '@shared/constants'; import type { startTUI as StartTUIFn } from '@ui/tui/start-tui'; import type { WizardStore } from '@ui/tui/store'; import { OutroKind, type WizardSession } from '@lib/wizard-session'; -import type { TaskStreamPush as TaskStreamPushClass } from '@lib/task-stream/task-stream-push'; +import type { TaskStreamPush as TaskStreamPushClass } from '@programs/task-stream/task-stream-push'; import { resolveNoTelemetry } from './resolve-no-telemetry'; import { checkLocalServices, getLocalDev } from '@shared/local-dev'; import { runCleanups } from '@utils/wizard-abort'; @@ -86,12 +86,12 @@ export function runWizard( const { startTUI } = await import('@ui/tui/start-tui'); const { buildSession, RunPhase } = await import('@lib/wizard-session'); - const { TaskStreamPush } = await import('@lib/task-stream/index'); + const { TaskStreamPush } = await import('@programs/task-stream/index'); const { PostHogDestination } = await import( - '@lib/task-stream/destinations/posthog' + '@programs/task-stream/destinations/posthog' ); const { createFileDestination } = await import( - '@lib/task-stream/destinations/file' + '@programs/task-stream/destinations/file' ); // Before the TUI mounts: once Ink owns the alt screen, anything written diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index e35ba2325..7d3fbf5ba 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -19,7 +19,7 @@ import { type Integration, type Sequence, } from '@shared/constants'; -import type { FrameworkConfig } from './framework-config'; +import type { FrameworkConfig } from '../programs/framework-config'; import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { SettingsConflict } from '@shared/claude-settings'; import type { ApiUser, ApiProject, Credentials } from '@shared/api'; diff --git a/src/lib/wizard-spellbook.ts b/src/lib/wizard-spellbook.ts index 8473d8096..4ecbd0687 100644 --- a/src/lib/wizard-spellbook.ts +++ b/src/lib/wizard-spellbook.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises'; import path from 'path'; import { getSkillsBaseUrl, POSTHOG_DOCS_URL } from '@shared/constants'; -import type { ProgramConfig } from './programs/program-step'; +import type { ProgramConfig } from '../programs/program-step'; import type { WizardSession } from './wizard-session'; import { downloadSkill } from '@agent'; import { diff --git a/src/lib/programs/__tests__/__snapshots__/flow-traces.test.ts.snap b/src/programs/__tests__/__snapshots__/flow-traces.test.ts.snap similarity index 100% rename from src/lib/programs/__tests__/__snapshots__/flow-traces.test.ts.snap rename to src/programs/__tests__/__snapshots__/flow-traces.test.ts.snap diff --git a/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap b/src/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap similarity index 100% rename from src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap rename to src/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap diff --git a/src/lib/programs/__tests__/agent-skill.test.ts b/src/programs/__tests__/agent-skill.test.ts similarity index 96% rename from src/lib/programs/__tests__/agent-skill.test.ts rename to src/programs/__tests__/agent-skill.test.ts index 913ce5d01..9f2111917 100644 --- a/src/lib/programs/__tests__/agent-skill.test.ts +++ b/src/programs/__tests__/agent-skill.test.ts @@ -2,8 +2,8 @@ import { createSkillProgram, AGENT_SKILL_STEPS, type SkillProgramOptions, -} from '@lib/programs/agent-skill/index'; -import type { ProgramRun } from '@lib/programs/program-run'; +} from '@programs/agent-skill/index'; +import type { ProgramRun } from '@programs/program-run'; import { buildSession, RunPhase } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; diff --git a/src/lib/programs/__tests__/audit-seed.test.ts b/src/programs/__tests__/audit-seed.test.ts similarity index 94% rename from src/lib/programs/__tests__/audit-seed.test.ts rename to src/programs/__tests__/audit-seed.test.ts index 1897a2b19..d8a92aa06 100644 --- a/src/lib/programs/__tests__/audit-seed.test.ts +++ b/src/programs/__tests__/audit-seed.test.ts @@ -2,8 +2,8 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { AUDIT_SEED_CHECKS, seedAuditLedger } from '@lib/programs/audit/seed'; -import { AUDIT_CHECKS_FILE, type AuditCheck } from '@lib/programs/audit/types'; +import { AUDIT_SEED_CHECKS, seedAuditLedger } from '@programs/audit/seed'; +import { AUDIT_CHECKS_FILE, type AuditCheck } from '@programs/audit/types'; import { COL_AREA_WIDTH } from '@ui/tui/screens/audit/AuditChecksViewer/layout'; const ids = (checks: AuditCheck[]) => checks.map((c) => c.id); diff --git a/src/lib/__tests__/cloudflare-detection.test.ts b/src/programs/__tests__/cloudflare-detection.test.ts similarity index 97% rename from src/lib/__tests__/cloudflare-detection.test.ts rename to src/programs/__tests__/cloudflare-detection.test.ts index b7c33cf7e..acaac9e20 100644 --- a/src/lib/__tests__/cloudflare-detection.test.ts +++ b/src/programs/__tests__/cloudflare-detection.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { detectCloudflareTarget } from '@lib/cloudflare-detection'; +import { detectCloudflareTarget } from '@programs/cloudflare-detection'; vi.mock('@utils/debug'); diff --git a/src/lib/programs/__tests__/detect-map.test.ts b/src/programs/__tests__/detect-map.test.ts similarity index 100% rename from src/lib/programs/__tests__/detect-map.test.ts rename to src/programs/__tests__/detect-map.test.ts diff --git a/src/lib/programs/__tests__/error-tracking.test.ts b/src/programs/__tests__/error-tracking.test.ts similarity index 88% rename from src/lib/programs/__tests__/error-tracking.test.ts rename to src/programs/__tests__/error-tracking.test.ts index bf49fb9c5..47f6b912b 100644 --- a/src/lib/programs/__tests__/error-tracking.test.ts +++ b/src/programs/__tests__/error-tracking.test.ts @@ -1,35 +1,37 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -import type { ProgramRun } from '@lib/programs/program-run'; +import type { ProgramRun } from '@programs/program-run'; import { Integration } from '@shared/constants'; -import type { AgenticDetectionReport } from '@lib/detection/agentic'; -import { detectFramework } from '@lib/detection/index'; +import type { AgenticDetectionReport } from '@programs/detection/agentic'; +import { detectFramework } from '@programs/detection/index'; import { ErrorCodes } from '@shared/errors'; -import { ERROR_TRACKING_TIPS } from '@lib/programs/error-tracking/content/tips'; +import { ERROR_TRACKING_TIPS } from '@ui/tui/decks/error-tracking/tips'; import { ERROR_TRACKING_PROJECT_PATH_KEY, toErrorTrackingReport, -} from '@lib/programs/error-tracking/detect-agentic'; +} from '@programs/error-tracking/detect-agentic'; import { errorTrackingConfig, SYMBOL_UPLOAD_CLI_FRAMEWORKS, -} from '@lib/programs/error-tracking/index'; -import { VARIANTS_REQUIRING_POSTHOG_CLI } from '@lib/programs/error-tracking-upload-source-maps/detect'; -import { preinstallPostHogCliOnce } from '@lib/programs/shared/posthog-cli-preinstall'; +} from '@programs/error-tracking/index'; +import { VARIANTS_REQUIRING_POSTHOG_CLI } from '@programs/error-tracking-upload-source-maps/detect'; +import { preinstallPostHogCliOnce } from '@programs/shared/posthog-cli-preinstall'; import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; -vi.mock('@lib/detection/index', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@programs/detection/index', async (importOriginal) => ({ + ...(await importOriginal()), detectFramework: vi.fn(), })); -vi.mock('@lib/detection/project-scope', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@programs/detection/project-scope', async (importOriginal) => ({ + ...(await importOriginal< + typeof import('@programs/detection/project-scope') + >()), scopeInstallDirToProject: vi.fn(), detectIntegrationProjects: vi.fn(), })); -vi.mock('@lib/programs/shared/posthog-cli-preinstall', () => ({ +vi.mock('@programs/shared/posthog-cli-preinstall', () => ({ preinstallPostHogCliOnce: vi.fn(), })); vi.mock('@utils/wizard-abort', async (importOriginal) => ({ diff --git a/src/lib/programs/__tests__/flow-traces.test.ts b/src/programs/__tests__/flow-traces.test.ts similarity index 99% rename from src/lib/programs/__tests__/flow-traces.test.ts rename to src/programs/__tests__/flow-traces.test.ts index 769c1759e..30f7ec314 100644 --- a/src/lib/programs/__tests__/flow-traces.test.ts +++ b/src/programs/__tests__/flow-traces.test.ts @@ -19,7 +19,7 @@ import { type WizardSession, } from '@lib/wizard-session'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { HostResolution } from '@shared/host-resolution'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/programs/__tests__/mcp-analytics.test.ts b/src/programs/__tests__/mcp-analytics.test.ts similarity index 97% rename from src/lib/programs/__tests__/mcp-analytics.test.ts rename to src/programs/__tests__/mcp-analytics.test.ts index a2c822c00..726515d6e 100644 --- a/src/lib/programs/__tests__/mcp-analytics.test.ts +++ b/src/programs/__tests__/mcp-analytics.test.ts @@ -1,7 +1,7 @@ import { MCP_ANALYTICS_ABORT_CASES, mcpAnalyticsConfig, -} from '@lib/programs/mcp-analytics/index'; +} from '@programs/mcp-analytics/index'; describe('MCP_ANALYTICS_ABORT_CASES', () => { // These are the exact `[ABORT] ` strings the mcp-analytics skill diff --git a/src/lib/programs/__tests__/metrics-program.test.ts b/src/programs/__tests__/metrics-program.test.ts similarity index 86% rename from src/lib/programs/__tests__/metrics-program.test.ts rename to src/programs/__tests__/metrics-program.test.ts index 46f381471..88657fd1a 100644 --- a/src/lib/programs/__tests__/metrics-program.test.ts +++ b/src/programs/__tests__/metrics-program.test.ts @@ -1,9 +1,9 @@ -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/programs/program-run'; +import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; +import { getProgramConfig, Program } from '@programs'; +import { metricsConfig } from '@programs/metrics/index'; +import type { ProgramRun } from '@programs/program-run'; -import { metricsCommand } from '../../../commands/metrics'; +import { metricsCommand } from '../../commands/metrics'; function staticRun(config: typeof metricsConfig): ProgramRun { if (typeof config.run === 'function') { diff --git a/src/lib/programs/__tests__/post-auth-gates.test.ts b/src/programs/__tests__/post-auth-gates.test.ts similarity index 100% rename from src/lib/programs/__tests__/post-auth-gates.test.ts rename to src/programs/__tests__/post-auth-gates.test.ts diff --git a/src/lib/programs/__tests__/posthog-cli-preinstall.test.ts b/src/programs/__tests__/posthog-cli-preinstall.test.ts similarity index 97% rename from src/lib/programs/__tests__/posthog-cli-preinstall.test.ts rename to src/programs/__tests__/posthog-cli-preinstall.test.ts index 355fd10b1..0da57bf9b 100644 --- a/src/lib/programs/__tests__/posthog-cli-preinstall.test.ts +++ b/src/programs/__tests__/posthog-cli-preinstall.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { preinstallPostHogCliOnce, resetPostHogCliPreinstallForTests, -} from '@lib/programs/shared/posthog-cli-preinstall'; +} from '@programs/shared/posthog-cli-preinstall'; import { installOrUpdatePostHogCli } from '@steps/install-cli-steering'; import { getUI } from '@ui'; import { analytics } from '@utils/analytics'; diff --git a/src/lib/programs/__tests__/posthog-integration-detect.test.ts b/src/programs/__tests__/posthog-integration-detect.test.ts similarity index 96% rename from src/lib/programs/__tests__/posthog-integration-detect.test.ts rename to src/programs/__tests__/posthog-integration-detect.test.ts index 7249ee81e..cb1e63416 100644 --- a/src/lib/programs/__tests__/posthog-integration-detect.test.ts +++ b/src/programs/__tests__/posthog-integration-detect.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { detectExistingPostHog } from '@lib/programs/posthog-integration/detect'; +import { detectExistingPostHog } from '@programs/posthog-integration/detect'; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'ph-detect-')); diff --git a/src/lib/programs/__tests__/program-registry.test.ts b/src/programs/__tests__/program-registry.test.ts similarity index 99% rename from src/lib/programs/__tests__/program-registry.test.ts rename to src/programs/__tests__/program-registry.test.ts index 90f3e472d..0450dccb4 100644 --- a/src/lib/programs/__tests__/program-registry.test.ts +++ b/src/programs/__tests__/program-registry.test.ts @@ -5,7 +5,7 @@ import { getLaunchablePrograms, getProgramConfig, getSubcommandPrograms, -} from '@lib/programs/program-registry'; +} from '../program-registry'; import type { WizardSession } from '@lib/wizard-session'; describe('PROGRAM_REGISTRY', () => { diff --git a/src/lib/programs/__tests__/program-step.test.ts b/src/programs/__tests__/program-step.test.ts similarity index 98% rename from src/lib/programs/__tests__/program-step.test.ts rename to src/programs/__tests__/program-step.test.ts index 57c4ac054..7bd68b2d4 100644 --- a/src/lib/programs/__tests__/program-step.test.ts +++ b/src/programs/__tests__/program-step.test.ts @@ -1,7 +1,7 @@ import { createProgramSequence, type ProgramStep, -} from '@lib/programs/program-step'; +} from '@programs/program-step'; describe('createProgramSequence', () => { it('filters out headless steps and keeps only screen-bearing ones', () => { diff --git a/src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts b/src/programs/__tests__/refresh-access-token-if-needed.test.ts similarity index 100% rename from src/lib/programs/__tests__/refresh-access-token-if-needed.test.ts rename to src/programs/__tests__/refresh-access-token-if-needed.test.ts diff --git a/src/lib/programs/__tests__/replay-vision.test.ts b/src/programs/__tests__/replay-vision.test.ts similarity index 97% rename from src/lib/programs/__tests__/replay-vision.test.ts rename to src/programs/__tests__/replay-vision.test.ts index c049a66b4..ec97a39fd 100644 --- a/src/lib/programs/__tests__/replay-vision.test.ts +++ b/src/programs/__tests__/replay-vision.test.ts @@ -4,7 +4,7 @@ import { Integration } from '@shared/constants'; import { replayVisionConfig, REPLAY_VISION_SUPPORTED, -} from '@lib/programs/replay-vision/index'; +} from '@programs/replay-vision/index'; describe('replay-vision program', () => { test('runs the replay-vision agent flow', () => { diff --git a/src/lib/programs/__tests__/revenue-analytics-detect.test.ts b/src/programs/__tests__/revenue-analytics-detect.test.ts similarity index 97% rename from src/lib/programs/__tests__/revenue-analytics-detect.test.ts rename to src/programs/__tests__/revenue-analytics-detect.test.ts index 5907dc4fa..33d6fabcb 100644 --- a/src/lib/programs/__tests__/revenue-analytics-detect.test.ts +++ b/src/programs/__tests__/revenue-analytics-detect.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { detectRevenuePrerequisites } from '@lib/programs/revenue-analytics/index'; +import { detectRevenuePrerequisites } from '@programs/revenue-analytics/index'; import { buildSession } from '@lib/wizard-session'; function makeTmpDir(): string { diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/programs/__tests__/run-agent-legacy.test.ts similarity index 98% rename from src/lib/programs/__tests__/run-agent-legacy.test.ts rename to src/programs/__tests__/run-agent-legacy.test.ts index 1fdce1e41..66df43539 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/programs/__tests__/run-agent-legacy.test.ts @@ -1,5 +1,5 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; -import { authenticate } from '@lib/programs/authenticate'; +import { authenticate } from '@programs/authenticate'; import { runProgramAgent } from '../run-agent-legacy'; import { runAgent, RunOutcome, type RunResult } from '@agent/runner'; import { Harness, Sequence } from '@shared/constants'; @@ -29,7 +29,7 @@ vi.mock('@agent/gateway-session', async (original) => ({ ...(await original()), configureGatewayFromCIEnvironment: vi.fn(), })); -vi.mock('@lib/task-stream/index', () => ({ +vi.mock('@programs/task-stream/index', () => ({ TaskStreamPush: class { attach = vi.fn(); shutdown = streamShutdown; @@ -54,7 +54,7 @@ vi.mock('@agent/runner', async (original) => ({ ...(await original()), runAgent: vi.fn(), })); -vi.mock('@lib/programs/authenticate', () => ({ +vi.mock('@programs/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), refreshAccessTokenIfNeeded: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/lib/programs/__tests__/self-driving-deck.test.ts b/src/programs/__tests__/self-driving-deck.test.ts similarity index 96% rename from src/lib/programs/__tests__/self-driving-deck.test.ts rename to src/programs/__tests__/self-driving-deck.test.ts index d4c552a23..94a39e55c 100644 --- a/src/lib/programs/__tests__/self-driving-deck.test.ts +++ b/src/programs/__tests__/self-driving-deck.test.ts @@ -7,7 +7,7 @@ */ import type { ReactNode, ReactElement } from 'react'; -import { getContentBlocks } from '@lib/programs/self-driving/content/index'; +import { getContentBlocks } from '@ui/tui/decks/self-driving/index'; /** paneWidth in LearnCard at 80 cols: (min(120, 80) - 2) / 2 - 2 */ const PANE_WIDTH_80COL = 37; diff --git a/src/lib/programs/__tests__/self-driving-detect.test.ts b/src/programs/__tests__/self-driving-detect.test.ts similarity index 98% rename from src/lib/programs/__tests__/self-driving-detect.test.ts rename to src/programs/__tests__/self-driving-detect.test.ts index a2ac4f929..2e20b4017 100644 --- a/src/lib/programs/__tests__/self-driving-detect.test.ts +++ b/src/programs/__tests__/self-driving-detect.test.ts @@ -5,23 +5,23 @@ import { detectSelfDrivingPrerequisites, selfDrivingConfig, SELF_DRIVING_ABORT_CASES, -} from '@lib/programs/self-driving/index'; +} from '@programs/self-driving/index'; import { detectPostHogPresent, POSTHOG_MANIFESTS, SELF_DRIVING_DETECTED_TOOLS_KEY, SELF_DRIVING_TOOL_KINDS, getSelfDrivingDetectedTools, -} from '@lib/programs/self-driving/detect'; -import { getDetectedWarehouseSources } from '@lib/programs/warehouse-source/detect'; +} from '@programs/self-driving/detect'; +import { getDetectedWarehouseSources } from '@programs/warehouse-source/detect'; import { WizardStore } from '@ui/tui/store'; -import { SOURCE_DETECTORS } from '@lib/warehouse-sources/registry'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; -import { toIntegrationReport } from '@lib/programs/self-driving/detect-agentic'; +import { SOURCE_DETECTORS } from '@programs/warehouse-sources/registry'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; +import { toIntegrationReport } from '@programs/self-driving/detect-agentic'; import { PROJECT_MANIFESTS, type AgenticDetectionReport, -} from '@lib/detection/agentic'; +} from '@programs/detection/agentic'; import { Integration } from '@shared/constants'; import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { buildSession } from '@lib/wizard-session'; diff --git a/src/lib/programs/__tests__/self-driving-prompt.test.ts b/src/programs/__tests__/self-driving-prompt.test.ts similarity index 97% rename from src/lib/programs/__tests__/self-driving-prompt.test.ts rename to src/programs/__tests__/self-driving-prompt.test.ts index a2b886c5d..8d8cacf8f 100644 --- a/src/lib/programs/__tests__/self-driving-prompt.test.ts +++ b/src/programs/__tests__/self-driving-prompt.test.ts @@ -1,7 +1,7 @@ -import { buildSelfDrivingPrompt } from '@lib/programs/self-driving/prompt'; +import { buildSelfDrivingPrompt } from '@programs/self-driving/prompt'; import type { PromptContext } from '@agent/agent-runner'; import { HostResolution } from '@shared/host-resolution'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; const ctx: PromptContext = { projectId: 123, diff --git a/src/lib/programs/__tests__/source-maps-detect-agentic.test.ts b/src/programs/__tests__/source-maps-detect-agentic.test.ts similarity index 99% rename from src/lib/programs/__tests__/source-maps-detect-agentic.test.ts rename to src/programs/__tests__/source-maps-detect-agentic.test.ts index a64c6973f..e0b6590d9 100644 --- a/src/lib/programs/__tests__/source-maps-detect-agentic.test.ts +++ b/src/programs/__tests__/source-maps-detect-agentic.test.ts @@ -6,8 +6,8 @@ import { goSdkVerifier, rustSdkVerifier, SOURCE_MAPS_TARGETS, -} from '@lib/programs/error-tracking-upload-source-maps/detect-agentic'; -import { AUTOMATABLE_VARIANTS } from '@lib/programs/error-tracking-upload-source-maps/detect'; +} from '@programs/error-tracking-upload-source-maps/detect-agentic'; +import { AUTOMATABLE_VARIANTS } from '@programs/error-tracking-upload-source-maps/detect'; describe('SOURCE_MAPS_TARGETS precedence', () => { const ids = SOURCE_MAPS_TARGETS.map((t) => t.id); diff --git a/src/lib/programs/__tests__/source-maps-detect.test.ts b/src/programs/__tests__/source-maps-detect.test.ts similarity index 99% rename from src/lib/programs/__tests__/source-maps-detect.test.ts rename to src/programs/__tests__/source-maps-detect.test.ts index 456933ad1..95546d373 100644 --- a/src/lib/programs/__tests__/source-maps-detect.test.ts +++ b/src/programs/__tests__/source-maps-detect.test.ts @@ -4,7 +4,7 @@ import * as os from 'os'; import { detectSourceMapsPrerequisites, SOURCE_MAPS_CONTEXT_KEYS, -} from '@lib/programs/error-tracking-upload-source-maps/index'; +} from '@programs/error-tracking-upload-source-maps/index'; import { buildSession } from '@lib/wizard-session'; function makeTmpDir(): string { diff --git a/src/lib/programs/__tests__/source-maps-prompt.test.ts b/src/programs/__tests__/source-maps-prompt.test.ts similarity index 97% rename from src/lib/programs/__tests__/source-maps-prompt.test.ts rename to src/programs/__tests__/source-maps-prompt.test.ts index 3945fdc50..588510502 100644 --- a/src/lib/programs/__tests__/source-maps-prompt.test.ts +++ b/src/programs/__tests__/source-maps-prompt.test.ts @@ -1,4 +1,4 @@ -import { buildSourceMapsUploadPrompt } from '@lib/programs/error-tracking-upload-source-maps/prompt'; +import { buildSourceMapsUploadPrompt } from '@programs/error-tracking-upload-source-maps/prompt'; const baseParams = { displayName: 'Node.js', diff --git a/src/lib/programs/__tests__/warehouse-ask-timeout.test.ts b/src/programs/__tests__/warehouse-ask-timeout.test.ts similarity index 94% rename from src/lib/programs/__tests__/warehouse-ask-timeout.test.ts rename to src/programs/__tests__/warehouse-ask-timeout.test.ts index 77850402d..55c9a781e 100644 --- a/src/lib/programs/__tests__/warehouse-ask-timeout.test.ts +++ b/src/programs/__tests__/warehouse-ask-timeout.test.ts @@ -18,7 +18,7 @@ vi.mock('@utils/analytics', () => ({ }, })); -import { warehouseSourceConfig } from '@lib/programs/warehouse-source/index'; +import { warehouseSourceConfig } from '@programs/warehouse-source/index'; import { LONGER_ASK_TIMEOUT_MS, DEFAULT_ASK_TIMEOUT_MS, diff --git a/src/lib/programs/__tests__/warehouse-seed-task.test.ts b/src/programs/__tests__/warehouse-seed-task.test.ts similarity index 94% rename from src/lib/programs/__tests__/warehouse-seed-task.test.ts rename to src/programs/__tests__/warehouse-seed-task.test.ts index 0a6458392..4d8dcc638 100644 --- a/src/lib/programs/__tests__/warehouse-seed-task.test.ts +++ b/src/programs/__tests__/warehouse-seed-task.test.ts @@ -4,7 +4,7 @@ * to answer — and the wizard, not the planner, is what decides that. */ import type { WizardSession } from '@lib/wizard-session'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; vi.mock('@utils/analytics', () => ({ analytics: { @@ -15,8 +15,8 @@ vi.mock('@utils/analytics', () => ({ }, })); -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; -import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@lib/programs/warehouse-source/detect'; +import { posthogIntegrationConfig } from '@programs/posthog-integration/index'; +import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect'; const POSTGRES: DetectedSource = { kind: 'Postgres', diff --git a/src/lib/programs/__tests__/warehouse-suggestion.test.ts b/src/programs/__tests__/warehouse-suggestion.test.ts similarity index 96% rename from src/lib/programs/__tests__/warehouse-suggestion.test.ts rename to src/programs/__tests__/warehouse-suggestion.test.ts index 0f6023f28..a3cd50191 100644 --- a/src/lib/programs/__tests__/warehouse-suggestion.test.ts +++ b/src/programs/__tests__/warehouse-suggestion.test.ts @@ -13,11 +13,11 @@ * say the run already connected the sources. */ -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; -import { POSTHOG_INTEGRATION_PROGRAM } from '@lib/programs/posthog-integration/steps'; -import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@lib/programs/warehouse-source/detect'; +import { posthogIntegrationConfig } from '@programs/posthog-integration/index'; +import { POSTHOG_INTEGRATION_PROGRAM } from '@programs/posthog-integration/steps'; +import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect'; import { buildSession, type WizardSession } from '@lib/wizard-session'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; const POSTGRES: DetectedSource = { kind: 'postgres', diff --git a/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts b/src/programs/__tests__/web-analytics-doctor-detect.test.ts similarity index 98% rename from src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts rename to src/programs/__tests__/web-analytics-doctor-detect.test.ts index 65f338d87..d755925a0 100644 --- a/src/lib/programs/__tests__/web-analytics-doctor-detect.test.ts +++ b/src/programs/__tests__/web-analytics-doctor-detect.test.ts @@ -5,7 +5,7 @@ import { detectWebAnalyticsPrerequisites, webAnalyticsDoctorConfig, WEB_ANALYTICS_ABORT_CASES, -} from '@lib/programs/web-analytics-doctor/index'; +} from '@programs/web-analytics-doctor/index'; import { WIZARD_TOOL_NAMES } from '@agent/tools'; import { buildSession } from '@lib/wizard-session'; diff --git a/src/lib/programs/agent-skill/index.ts b/src/programs/agent-skill/index.ts similarity index 92% rename from src/lib/programs/agent-skill/index.ts rename to src/programs/agent-skill/index.ts index e59f3fdf1..75752731e 100644 --- a/src/lib/programs/agent-skill/index.ts +++ b/src/programs/agent-skill/index.ts @@ -19,11 +19,11 @@ * }) */ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; import type { AbortCase } from '@agent/types'; -import type { ProgramRun } from '@lib/programs/program-run'; +import type { ProgramRun } from '@programs/program-run'; import { AGENT_SKILL_STEPS } from './steps.js'; -import { getContentBlocks } from './content/index.js'; +import { getContentBlocks } from '../../ui/tui/decks/agent-skill/index.js'; export interface SkillProgramOptions { /** Context-mill skill ID to install */ diff --git a/src/lib/programs/agent-skill/steps.ts b/src/programs/agent-skill/steps.ts similarity index 87% rename from src/lib/programs/agent-skill/steps.ts rename to src/programs/agent-skill/steps.ts index 992726616..faafee60b 100644 --- a/src/lib/programs/agent-skill/steps.ts +++ b/src/programs/agent-skill/steps.ts @@ -5,9 +5,9 @@ * No detection, no setup, no MCP. */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import { RunPhase } from '@lib/wizard-session'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; export const AGENT_SKILL_STEPS: ProgramStep[] = [ { diff --git a/src/lib/programs/ai-observability/index.ts b/src/programs/ai-observability/index.ts similarity index 93% rename from src/lib/programs/ai-observability/index.ts rename to src/programs/ai-observability/index.ts index d86c8ce7e..9b0311621 100644 --- a/src/lib/programs/ai-observability/index.ts +++ b/src/programs/ai-observability/index.ts @@ -1,6 +1,6 @@ -import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; -import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/index'; -import { getContentBlocks } from '@lib/programs/agent-skill/content/index'; +import type { ProgramConfig, ProgramStep } from '@programs/program-step'; +import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; +import { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; import { headlessOption, regionOption } from '@lib/headless-mode'; const AI_OBSERVABILITY_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => diff --git a/src/lib/programs/ai-observability/test/e2e.json b/src/programs/ai-observability/test/e2e.json similarity index 100% rename from src/lib/programs/ai-observability/test/e2e.json rename to src/programs/ai-observability/test/e2e.json diff --git a/src/lib/programs/ai-opt-in-gate.ts b/src/programs/ai-opt-in-gate.ts similarity index 100% rename from src/lib/programs/ai-opt-in-gate.ts rename to src/programs/ai-opt-in-gate.ts diff --git a/src/lib/programs/audit/detect.ts b/src/programs/audit/detect.ts similarity index 100% rename from src/lib/programs/audit/detect.ts rename to src/programs/audit/detect.ts diff --git a/src/lib/programs/audit/index.ts b/src/programs/audit/index.ts similarity index 95% rename from src/lib/programs/audit/index.ts rename to src/programs/audit/index.ts index a5921edbe..770668c17 100644 --- a/src/lib/programs/audit/index.ts +++ b/src/programs/audit/index.ts @@ -1,9 +1,9 @@ import { AGENT_SKILL_STEPS, createSkillProgram, -} from '@lib/programs/agent-skill/index'; -import type { ProgramStep, ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/programs/program-run'; +} from '@programs/agent-skill/index'; +import type { ProgramStep, ProgramConfig } from '@programs/program-step'; +import type { ProgramRun } from '@programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; import { WIZARD_TOOL_NAMES } from '@agent'; diff --git a/src/lib/programs/audit/ledger-watcher.ts b/src/programs/audit/ledger-watcher.ts similarity index 100% rename from src/lib/programs/audit/ledger-watcher.ts rename to src/programs/audit/ledger-watcher.ts diff --git a/src/lib/programs/audit/seed.ts b/src/programs/audit/seed.ts similarity index 100% rename from src/lib/programs/audit/seed.ts rename to src/programs/audit/seed.ts diff --git a/src/lib/programs/audit/test/e2e.json b/src/programs/audit/test/e2e.json similarity index 100% rename from src/lib/programs/audit/test/e2e.json rename to src/programs/audit/test/e2e.json diff --git a/src/lib/programs/audit/types.ts b/src/programs/audit/types.ts similarity index 100% rename from src/lib/programs/audit/types.ts rename to src/programs/audit/types.ts diff --git a/src/lib/programs/authenticate.ts b/src/programs/authenticate.ts similarity index 98% rename from src/lib/programs/authenticate.ts rename to src/programs/authenticate.ts index 109adc9f4..a5271c228 100644 --- a/src/lib/programs/authenticate.ts +++ b/src/programs/authenticate.ts @@ -11,7 +11,7 @@ */ import type { Credentials, WizardSession } from '@lib/wizard-session'; -import type { ProgramId } from '@lib/programs/program-registry'; +import type { ProgramId } from '@programs/program-registry'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { refreshAccessToken } from '@utils/oauth'; import { OAuthError } from '@utils/oauth-errors'; diff --git a/src/lib/cloudflare-detection.ts b/src/programs/cloudflare-detection.ts similarity index 100% rename from src/lib/cloudflare-detection.ts rename to src/programs/cloudflare-detection.ts diff --git a/src/lib/programs/detect-map.ts b/src/programs/detect-map.ts similarity index 81% rename from src/lib/programs/detect-map.ts rename to src/programs/detect-map.ts index aaf4e25e2..ba1228d79 100644 --- a/src/lib/programs/detect-map.ts +++ b/src/programs/detect-map.ts @@ -1,9 +1,9 @@ import { ErrorCodes, type ErrorCode } from '@shared/errors'; -import type { RevenueDetectError } from '@lib/programs/revenue-analytics/detect'; -import type { SelfDrivingDetectError } from '@lib/programs/self-driving/detect'; -import type { SourceMapsDetectError } from '@lib/programs/error-tracking-upload-source-maps/detect'; -import type { WarehouseDetectError } from '@lib/programs/warehouse-source/detect'; -import type { WebAnalyticsDetectError } from '@lib/programs/web-analytics-doctor/detect'; +import type { RevenueDetectError } from '@programs/revenue-analytics/detect'; +import type { SelfDrivingDetectError } from '@programs/self-driving/detect'; +import type { SourceMapsDetectError } from '@programs/error-tracking-upload-source-maps/detect'; +import type { WarehouseDetectError } from '@programs/warehouse-source/detect'; +import type { WebAnalyticsDetectError } from '@programs/web-analytics-doctor/detect'; /** * Every `kind` a program detect step can write into diff --git a/src/lib/detection/__tests__/agentic-progress.test.ts b/src/programs/detection/__tests__/agentic-progress.test.ts similarity index 100% rename from src/lib/detection/__tests__/agentic-progress.test.ts rename to src/programs/detection/__tests__/agentic-progress.test.ts diff --git a/src/lib/detection/__tests__/agentic.test.ts b/src/programs/detection/__tests__/agentic.test.ts similarity index 99% rename from src/lib/detection/__tests__/agentic.test.ts rename to src/programs/detection/__tests__/agentic.test.ts index c63335c74..876b617f4 100644 --- a/src/lib/detection/__tests__/agentic.test.ts +++ b/src/programs/detection/__tests__/agentic.test.ts @@ -3,7 +3,7 @@ import { deriveReportJson, manifestGlob, resolveProjectDir, -} from '@lib/detection/agentic'; +} from '@programs/detection/agentic'; const TARGETS = ['nextjs', 'node', 'vite']; diff --git a/src/lib/detection/__tests__/context.test.ts b/src/programs/detection/__tests__/context.test.ts similarity index 96% rename from src/lib/detection/__tests__/context.test.ts rename to src/programs/detection/__tests__/context.test.ts index 138b94d0c..acb64478d 100644 --- a/src/lib/detection/__tests__/context.test.ts +++ b/src/programs/detection/__tests__/context.test.ts @@ -1,8 +1,8 @@ import { gatherFrameworkContext, checkFrameworkVersion, -} from '@lib/detection/context'; -import type { FrameworkConfig } from '@lib/framework-config'; +} from '@programs/detection/context'; +import type { FrameworkConfig } from '@programs/types'; import type { WizardRunOptions } from '@utils/types'; const baseOptions: WizardRunOptions = { diff --git a/src/lib/detection/__tests__/features.test.ts b/src/programs/detection/__tests__/features.test.ts similarity index 98% rename from src/lib/detection/__tests__/features.test.ts rename to src/programs/detection/__tests__/features.test.ts index a5a21ff7f..35602ab8f 100644 --- a/src/lib/detection/__tests__/features.test.ts +++ b/src/programs/detection/__tests__/features.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { discoverFeatures } from '@lib/detection/features'; +import { discoverFeatures } from '@programs/detection/features'; import { DiscoveredFeature } from '@lib/wizard-session'; function makeTmpDir(): string { diff --git a/src/lib/detection/__tests__/framework.test.ts b/src/programs/detection/__tests__/framework.test.ts similarity index 96% rename from src/lib/detection/__tests__/framework.test.ts rename to src/programs/detection/__tests__/framework.test.ts index cd2a333f0..80217cedc 100644 --- a/src/lib/detection/__tests__/framework.test.ts +++ b/src/programs/detection/__tests__/framework.test.ts @@ -1,15 +1,15 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { detectFramework } from '@lib/detection/framework'; +import { detectFramework } from '@programs/detection/framework'; import { Integration } from '@shared/constants'; -import { ANDROID_AGENT_CONFIG } from '../../../frameworks/android/android-wizard-agent'; -import { KMP_AGENT_CONFIG } from '../../../frameworks/kmp/kmp-wizard-agent'; -import { ELIXIR_AGENT_CONFIG } from '../../../frameworks/elixir/elixir-wizard-agent'; -import { GO_AGENT_CONFIG } from '../../../frameworks/go/go-wizard-agent'; -import { JAVA_AGENT_CONFIG } from '../../../frameworks/java/java-wizard-agent'; -import { RUST_AGENT_CONFIG } from '../../../frameworks/rust/rust-wizard-agent'; -import { FLUTTER_AGENT_CONFIG } from '../../../frameworks/flutter/flutter-wizard-agent'; +import { ANDROID_AGENT_CONFIG } from '../../frameworks/android/android-wizard-agent'; +import { KMP_AGENT_CONFIG } from '../../frameworks/kmp/kmp-wizard-agent'; +import { ELIXIR_AGENT_CONFIG } from '../../frameworks/elixir/elixir-wizard-agent'; +import { GO_AGENT_CONFIG } from '../../frameworks/go/go-wizard-agent'; +import { JAVA_AGENT_CONFIG } from '../../frameworks/java/java-wizard-agent'; +import { RUST_AGENT_CONFIG } from '../../frameworks/rust/rust-wizard-agent'; +import { FLUTTER_AGENT_CONFIG } from '../../frameworks/flutter/flutter-wizard-agent'; /** A throwaway project dir seeded with the given files. */ function makeProject(files: Record): string { diff --git a/src/lib/detection/__tests__/package-manager.test.ts b/src/programs/detection/__tests__/package-manager.test.ts similarity index 99% rename from src/lib/detection/__tests__/package-manager.test.ts rename to src/programs/detection/__tests__/package-manager.test.ts index 3200bf4fd..ae102520a 100644 --- a/src/lib/detection/__tests__/package-manager.test.ts +++ b/src/programs/detection/__tests__/package-manager.test.ts @@ -12,7 +12,7 @@ import { mixPackageManager, goModulesPackageManager, pubPackageManager, -} from '@lib/detection/package-manager'; +} from '@programs/detection/package-manager'; vi.mock('@utils/debug'); vi.mock('../../../telemetry', () => ({ diff --git a/src/lib/detection/__tests__/project-scope.test.ts b/src/programs/detection/__tests__/project-scope.test.ts similarity index 95% rename from src/lib/detection/__tests__/project-scope.test.ts rename to src/programs/detection/__tests__/project-scope.test.ts index c33db784d..6b897d7e1 100644 --- a/src/lib/detection/__tests__/project-scope.test.ts +++ b/src/programs/detection/__tests__/project-scope.test.ts @@ -1,25 +1,25 @@ import { detectProjectsWithAgent, type AgenticProject, -} from '@lib/detection/agentic'; +} from '@programs/detection/agentic'; import { chooseIntegrationProject, scopeInstallDirToProject, -} from '@lib/detection/project-scope'; +} from '@programs/detection/project-scope'; import { AGENTIC_DETECTION_TIMEOUT_MS, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, } from '@shared/constants'; -import { authenticate } from '@lib/programs/authenticate'; +import { authenticate } from '@programs/authenticate'; import { buildSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; // Mock only the two network edges of scopeInstallDirToProject; everything else runs real. -vi.mock('@lib/programs/authenticate', () => ({ +vi.mock('@programs/authenticate', () => ({ authenticate: vi.fn().mockResolvedValue(undefined), })); -vi.mock('@lib/detection/agentic', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@programs/detection/agentic', async (importOriginal) => ({ + ...(await importOriginal()), detectProjectsWithAgent: vi.fn(), })); diff --git a/src/lib/detection/agentic.ts b/src/programs/detection/agentic.ts similarity index 99% rename from src/lib/detection/agentic.ts rename to src/programs/detection/agentic.ts index e3f96aa22..93d6b9803 100644 --- a/src/lib/detection/agentic.ts +++ b/src/programs/detection/agentic.ts @@ -58,7 +58,7 @@ export type DetectEvent = (line: string) => void; * are identified by. One brace-expansion Glob over all of these locates every * project root in the repo in a single call, regardless of language. * - * Counterpart: POSTHOG_MANIFESTS in @lib/programs/self-driving/detect (SDK + * Counterpart: POSTHOG_MANIFESTS in @programs/self-driving/detect (SDK * grep); keep the two in sync. */ export const PROJECT_MANIFESTS: readonly string[] = [ diff --git a/src/lib/detection/context.ts b/src/programs/detection/context.ts similarity index 97% rename from src/lib/detection/context.ts rename to src/programs/detection/context.ts index 8e0d845d7..23ab66832 100644 --- a/src/lib/detection/context.ts +++ b/src/programs/detection/context.ts @@ -8,7 +8,7 @@ import * as semver from 'semver'; import { DETECTION_TIMEOUT_MS } from '@shared/constants'; -import type { FrameworkConfig } from '@lib/framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import type { WizardRunOptions } from '@utils/types'; /** diff --git a/src/lib/detection/features.ts b/src/programs/detection/features.ts similarity index 100% rename from src/lib/detection/features.ts rename to src/programs/detection/features.ts diff --git a/src/lib/detection/framework.ts b/src/programs/detection/framework.ts similarity index 95% rename from src/lib/detection/framework.ts rename to src/programs/detection/framework.ts index 07dfa2c35..01b4f2105 100644 --- a/src/lib/detection/framework.ts +++ b/src/programs/detection/framework.ts @@ -7,7 +7,7 @@ */ import { Integration, DETECTION_TIMEOUT_MS } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; /** * Loop through all registered frameworks and return the first one diff --git a/src/lib/detection/index.ts b/src/programs/detection/index.ts similarity index 100% rename from src/lib/detection/index.ts rename to src/programs/detection/index.ts diff --git a/src/lib/detection/package-manager.ts b/src/programs/detection/package-manager.ts similarity index 99% rename from src/lib/detection/package-manager.ts rename to src/programs/detection/package-manager.ts index 8212dd1f7..b4af311ae 100644 --- a/src/lib/detection/package-manager.ts +++ b/src/programs/detection/package-manager.ts @@ -29,7 +29,7 @@ export type { import { detectPackageManager as detectPythonPM, PythonPackageManager, -} from '@frameworks/python/utils'; +} from '@programs/frameworks/python/utils'; // --------------------------------------------------------------------------- // Python helper diff --git a/src/lib/detection/project-scope.ts b/src/programs/detection/project-scope.ts similarity index 98% rename from src/lib/detection/project-scope.ts rename to src/programs/detection/project-scope.ts index 0cf7ace18..08b3db28c 100644 --- a/src/lib/detection/project-scope.ts +++ b/src/programs/detection/project-scope.ts @@ -8,8 +8,8 @@ import { type DetectEvent, type DetectTarget, } from './agentic.js'; -import { authenticate } from '@lib/programs/authenticate'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { authenticate } from '@programs/authenticate'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { AGENTIC_DETECTION_TIMEOUT_MS, Integration, diff --git a/src/lib/programs/dispatch-family.ts b/src/programs/dispatch-family.ts similarity index 93% rename from src/lib/programs/dispatch-family.ts rename to src/programs/dispatch-family.ts index b9da9d39f..b92a25b6a 100644 --- a/src/lib/programs/dispatch-family.ts +++ b/src/programs/dispatch-family.ts @@ -1,17 +1,17 @@ import type { Arguments } from 'yargs'; -import { auditConfig } from '@lib/programs/audit/index'; -import { AUDIT_CHECKS_FILE } from '@lib/programs/audit/types'; +import { auditConfig } from '@programs/audit/index'; +import { AUDIT_CHECKS_FILE } from '@programs/audit/types'; import { WIZARD_TOOL_NAMES } from '@agent'; -import { agentSkillConfig } from '@lib/programs/program-registry'; -import { webAnalyticsDoctorConfig } from '@lib/programs/web-analytics-doctor/index'; -import type { ProgramConfig } from '@lib/programs/program-step'; +import { agentSkillConfig } from '@programs/program-registry'; +import { webAnalyticsDoctorConfig } from '@programs/web-analytics-doctor/index'; +import type { ProgramConfig } from '@programs/program-step'; import { getSkillsBaseUrl } from '@shared/constants'; import { fetchSkillMenu, type CliEntry } from '@shared/skill-menu'; import { analytics } from '@utils/analytics'; -import { dispatchProgram } from '../../commands/factories/shared'; -import type { Command } from '../../commands/command'; +import { dispatchProgram } from '../commands/factories/shared'; +import type { Command } from '../commands/command'; import { ErrorCodes } from '@shared/errors'; import { emitWizardError } from '@shared/errors'; diff --git a/src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts b/src/programs/error-tracking-upload-source-maps/detect-agentic.ts similarity index 99% rename from src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts rename to src/programs/error-tracking-upload-source-maps/detect-agentic.ts index 17d3561c0..033635b19 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts +++ b/src/programs/error-tracking-upload-source-maps/detect-agentic.ts @@ -1,6 +1,6 @@ /** * Source-maps adapter over the generic agentic detector - * (`@lib/detection/agentic`). The detector itself is product-knowledge-free; + * (`@programs/detection/agentic`). The detector itself is product-knowledge-free; * this file supplies the source-maps targets (the automatable skill variants), * maps the result back to variants, and classifies each project as * instrumentable or not. The screen renders the result and the run step @@ -16,7 +16,7 @@ import { type DetectTarget, type AgenticDetectionReport, type DetectEvent, -} from '@lib/detection/agentic'; +} from '@programs/detection/agentic'; import type { WizardSession } from '@lib/wizard-session'; import { VARIANT_DISPLAY_NAME, diff --git a/src/lib/programs/error-tracking-upload-source-maps/detect.ts b/src/programs/error-tracking-upload-source-maps/detect.ts similarity index 100% rename from src/lib/programs/error-tracking-upload-source-maps/detect.ts rename to src/programs/error-tracking-upload-source-maps/detect.ts diff --git a/src/lib/programs/error-tracking-upload-source-maps/index.ts b/src/programs/error-tracking-upload-source-maps/index.ts similarity index 94% rename from src/lib/programs/error-tracking-upload-source-maps/index.ts rename to src/programs/error-tracking-upload-source-maps/index.ts index afdc17007..101d42594 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/index.ts +++ b/src/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/programs/program-run'; +import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramRun } from '@programs/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'; @@ -13,9 +13,9 @@ import { VARIANTS_REQUIRING_POSTHOG_CLI, type SkillVariant, } from './detect.js'; -import { getContentBlocks } from './content/index.js'; +import { getContentBlocks } from '../../ui/tui/decks/error-tracking-upload-source-maps/index.js'; import { getUI } from '@ui'; -import { preinstallPostHogCliOnce } from '@lib/programs/shared/posthog-cli-preinstall'; +import { preinstallPostHogCliOnce } from '@programs/shared/posthog-cli-preinstall'; const REPORT_FILE = 'posthog-source-maps-report.md'; const DOCS_URL = 'https://posthog.com/docs/error-tracking/upload-source-maps'; diff --git a/src/lib/programs/error-tracking-upload-source-maps/prompt.ts b/src/programs/error-tracking-upload-source-maps/prompt.ts similarity index 100% rename from src/lib/programs/error-tracking-upload-source-maps/prompt.ts rename to src/programs/error-tracking-upload-source-maps/prompt.ts diff --git a/src/lib/programs/error-tracking-upload-source-maps/steps.ts b/src/programs/error-tracking-upload-source-maps/steps.ts similarity index 96% rename from src/lib/programs/error-tracking-upload-source-maps/steps.ts rename to src/programs/error-tracking-upload-source-maps/steps.ts index 33bfc7449..466ae9074 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/steps.ts +++ b/src/programs/error-tracking-upload-source-maps/steps.ts @@ -7,7 +7,7 @@ * needs credentials. */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import type { WizardSession } from '@lib/wizard-session'; import { RunPhase } from '@lib/wizard-session'; import { SOURCE_MAPS_CONTEXT_KEYS } from './detect.js'; diff --git a/src/lib/programs/error-tracking-upload-source-maps/test/e2e.json b/src/programs/error-tracking-upload-source-maps/test/e2e.json similarity index 100% rename from src/lib/programs/error-tracking-upload-source-maps/test/e2e.json rename to src/programs/error-tracking-upload-source-maps/test/e2e.json diff --git a/src/lib/programs/error-tracking/detect-agentic.ts b/src/programs/error-tracking/detect-agentic.ts similarity index 96% rename from src/lib/programs/error-tracking/detect-agentic.ts rename to src/programs/error-tracking/detect-agentic.ts index 1967b79c4..654de6c68 100644 --- a/src/lib/programs/error-tracking/detect-agentic.ts +++ b/src/programs/error-tracking/detect-agentic.ts @@ -16,12 +16,12 @@ import { resolveProjectDir, type AgenticDetectionReport, type DetectEvent, -} from '@lib/detection/agentic'; -import { gatherFrameworkContext } from '@lib/detection/index'; +} from '@programs/detection/agentic'; +import { gatherFrameworkContext } from '@programs/detection/index'; import { detectIntegrationProjects, toIntegrationCandidates, -} from '@lib/detection/project-scope'; +} from '@programs/detection/project-scope'; import type { WizardSession } from '@lib/wizard-session'; /** frameworkContext key for the picked project's path, relative to the repo root. */ diff --git a/src/lib/programs/error-tracking/index.ts b/src/programs/error-tracking/index.ts similarity index 92% rename from src/lib/programs/error-tracking/index.ts rename to src/programs/error-tracking/index.ts index b28ae779d..12924b116 100644 --- a/src/lib/programs/error-tracking/index.ts +++ b/src/programs/error-tracking/index.ts @@ -1,19 +1,19 @@ import { Integration } from '@shared/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/programs/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'; +import { detectFramework } from '@programs/detection/index'; +import { scopeInstallDirToProject } from '@programs/detection/project-scope'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import type { ProgramRun } from '@programs/program-run'; +import { AGENT_SKILL_STEPS } from '@programs/agent-skill/steps'; +import { getContentBlocks } from '@ui/tui/decks/error-tracking/index'; +import { getTips } from '@ui/tui/decks/error-tracking/tips'; import { ERROR_TRACKING_UNSUPPORTED, errorTrackingProjectDir, gatherErrorTrackingContext, -} from '@lib/programs/error-tracking/detect-agentic'; -import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; +} from '@programs/error-tracking/detect-agentic'; +import type { ProgramConfig, ProgramStep } from '@programs/program-step'; import type { WizardSession } from '@lib/wizard-session'; -import { preinstallPostHogCliOnce } from '@lib/programs/shared/posthog-cli-preinstall'; +import { preinstallPostHogCliOnce } from '@programs/shared/posthog-cli-preinstall'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; diff --git a/src/lib/programs/error-tracking/test/e2e.json b/src/programs/error-tracking/test/e2e.json similarity index 100% rename from src/lib/programs/error-tracking/test/e2e.json rename to src/programs/error-tracking/test/e2e.json diff --git a/src/lib/programs/events-audit/constants.ts b/src/programs/events-audit/constants.ts similarity index 100% rename from src/lib/programs/events-audit/constants.ts rename to src/programs/events-audit/constants.ts diff --git a/src/lib/programs/events-audit/index.ts b/src/programs/events-audit/index.ts similarity index 91% rename from src/lib/programs/events-audit/index.ts rename to src/programs/events-audit/index.ts index d072c7f60..c0de7ae5d 100644 --- a/src/lib/programs/events-audit/index.ts +++ b/src/programs/events-audit/index.ts @@ -1,17 +1,17 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/programs/program-run'; +import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramRun } from '@programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { OutroKind } from '@lib/wizard-session'; -import { SPINNER_MESSAGE } from '@lib/framework-config'; +import { SPINNER_MESSAGE } from '@programs/framework-config'; import { isUsingTypeScript } from '@utils/setup-utils'; import { WIZARD_TOOL_NAMES } from '@agent'; import { EVENTS_AUDIT_PROGRAM } from './steps.js'; -import { AUDIT_CHECKS_FILE, AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; -import { seedAuditLedger } from '@lib/programs/audit/seed'; +import { AUDIT_CHECKS_FILE, AUDIT_CHECKS_KEY } from '@programs/audit/types'; +import { seedAuditLedger } from '@programs/audit/seed'; import { EVENTS_AUDIT_SEED_CHECKS } from './seed.js'; // SETUP_REPORT_FILE is also re-exported for backward compat with existing -// imports from `@lib/programs/events-audit`. EVENT_INVENTORY_FILE and +// imports from `@programs/events-audit`. EVENT_INVENTORY_FILE and // EVENT_INVENTORY_PART_PATTERN are only used by yara-hooks, which imports // them directly from `./constants` — no re-export needed. import { SETUP_REPORT_FILE } from './constants.js'; diff --git a/src/lib/programs/events-audit/seed.ts b/src/programs/events-audit/seed.ts similarity index 96% rename from src/lib/programs/events-audit/seed.ts rename to src/programs/events-audit/seed.ts index f2c0df189..ef480aac0 100644 --- a/src/lib/programs/events-audit/seed.ts +++ b/src/programs/events-audit/seed.ts @@ -1,4 +1,4 @@ -import type { AuditCheck } from '@lib/programs/audit/types'; +import type { AuditCheck } from '@programs/audit/types'; /** * The 7 phases the events-audit skill marches through. One check per area diff --git a/src/lib/programs/events-audit/steps.ts b/src/programs/events-audit/steps.ts similarity index 92% rename from src/lib/programs/events-audit/steps.ts rename to src/programs/events-audit/steps.ts index b9e07298a..53b2f59a2 100644 --- a/src/lib/programs/events-audit/steps.ts +++ b/src/programs/events-audit/steps.ts @@ -8,10 +8,10 @@ * logic) instead of the integration intro. */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import type { WizardSession } from '@lib/wizard-session'; import { RunPhase } from '@lib/wizard-session'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; function needsSetup(session: WizardSession): boolean { const config = session.frameworkConfig; diff --git a/src/lib/framework-config.ts b/src/programs/framework-config.ts similarity index 100% rename from src/lib/framework-config.ts rename to src/programs/framework-config.ts diff --git a/src/frameworks/android/android-wizard-agent.ts b/src/programs/frameworks/android/android-wizard-agent.ts similarity index 96% rename from src/frameworks/android/android-wizard-agent.ts rename to src/programs/frameworks/android/android-wizard-agent.ts index 789cf3746..f2f81227b 100644 --- a/src/frameworks/android/android-wizard-agent.ts +++ b/src/programs/frameworks/android/android-wizard-agent.ts @@ -1,6 +1,6 @@ /* Android (Kotlin) wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import * as fs from 'node:fs'; @@ -10,7 +10,7 @@ import { getKotlinVersionBucket, getMinSdkVersion, } from './utils'; -import { gradlePackageManager } from '@lib/detection/package-manager'; +import { gradlePackageManager } from '@programs/detection/package-manager'; type AndroidContext = { kotlinVersion?: string; diff --git a/src/frameworks/android/utils.ts b/src/programs/frameworks/android/utils.ts similarity index 100% rename from src/frameworks/android/utils.ts rename to src/programs/frameworks/android/utils.ts diff --git a/src/frameworks/angular/angular-wizard-agent.ts b/src/programs/frameworks/angular/angular-wizard-agent.ts similarity index 94% rename from src/frameworks/angular/angular-wizard-agent.ts rename to src/programs/frameworks/angular/angular-wizard-agent.ts index bd1fdc675..e746a1851 100644 --- a/src/frameworks/angular/angular-wizard-agent.ts +++ b/src/programs/frameworks/angular/angular-wizard-agent.ts @@ -1,7 +1,7 @@ /* Angular wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/angular/utils.ts b/src/programs/frameworks/angular/utils.ts similarity index 100% rename from src/frameworks/angular/utils.ts rename to src/programs/frameworks/angular/utils.ts diff --git a/src/frameworks/astro/astro-wizard-agent.ts b/src/programs/frameworks/astro/astro-wizard-agent.ts similarity index 97% rename from src/frameworks/astro/astro-wizard-agent.ts rename to src/programs/frameworks/astro/astro-wizard-agent.ts index f8052cc30..a84ad23b3 100644 --- a/src/frameworks/astro/astro-wizard-agent.ts +++ b/src/programs/frameworks/astro/astro-wizard-agent.ts @@ -1,7 +1,7 @@ /* Astro wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/astro/utils.ts b/src/programs/frameworks/astro/utils.ts similarity index 100% rename from src/frameworks/astro/utils.ts rename to src/programs/frameworks/astro/utils.ts diff --git a/src/frameworks/django/django-wizard-agent.ts b/src/programs/frameworks/django/django-wizard-agent.ts similarity index 95% rename from src/frameworks/django/django-wizard-agent.ts rename to src/programs/frameworks/django/django-wizard-agent.ts index 6abe33c2a..6c93bbc4e 100644 --- a/src/frameworks/django/django-wizard-agent.ts +++ b/src/programs/frameworks/django/django-wizard-agent.ts @@ -1,8 +1,8 @@ /* Django wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; -import { detectPythonPackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; +import { detectPythonPackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; diff --git a/src/frameworks/django/utils.ts b/src/programs/frameworks/django/utils.ts similarity index 100% rename from src/frameworks/django/utils.ts rename to src/programs/frameworks/django/utils.ts diff --git a/src/frameworks/elixir/elixir-wizard-agent.ts b/src/programs/frameworks/elixir/elixir-wizard-agent.ts similarity index 96% rename from src/frameworks/elixir/elixir-wizard-agent.ts rename to src/programs/frameworks/elixir/elixir-wizard-agent.ts index f74b4e38e..b617f74b9 100644 --- a/src/frameworks/elixir/elixir-wizard-agent.ts +++ b/src/programs/frameworks/elixir/elixir-wizard-agent.ts @@ -2,8 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { mixPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { mixPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; type ElixirContext = { diff --git a/src/frameworks/fastapi/fastapi-wizard-agent.ts b/src/programs/frameworks/fastapi/fastapi-wizard-agent.ts similarity index 96% rename from src/frameworks/fastapi/fastapi-wizard-agent.ts rename to src/programs/frameworks/fastapi/fastapi-wizard-agent.ts index 1a6763dbd..22326fc49 100644 --- a/src/frameworks/fastapi/fastapi-wizard-agent.ts +++ b/src/programs/frameworks/fastapi/fastapi-wizard-agent.ts @@ -1,8 +1,8 @@ /* FastAPI wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; -import { detectPythonPackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; +import { detectPythonPackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getFastAPIVersion, diff --git a/src/frameworks/fastapi/utils.ts b/src/programs/frameworks/fastapi/utils.ts similarity index 100% rename from src/frameworks/fastapi/utils.ts rename to src/programs/frameworks/fastapi/utils.ts diff --git a/src/frameworks/flask/flask-wizard-agent.ts b/src/programs/frameworks/flask/flask-wizard-agent.ts similarity index 95% rename from src/frameworks/flask/flask-wizard-agent.ts rename to src/programs/frameworks/flask/flask-wizard-agent.ts index 8d143fda6..b781777fb 100644 --- a/src/frameworks/flask/flask-wizard-agent.ts +++ b/src/programs/frameworks/flask/flask-wizard-agent.ts @@ -1,8 +1,8 @@ /* Flask wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; -import { detectPythonPackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; +import { detectPythonPackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; diff --git a/src/frameworks/flask/utils.ts b/src/programs/frameworks/flask/utils.ts similarity index 100% rename from src/frameworks/flask/utils.ts rename to src/programs/frameworks/flask/utils.ts diff --git a/src/frameworks/flutter/flutter-wizard-agent.ts b/src/programs/frameworks/flutter/flutter-wizard-agent.ts similarity index 96% rename from src/frameworks/flutter/flutter-wizard-agent.ts rename to src/programs/frameworks/flutter/flutter-wizard-agent.ts index c2fb22371..5a2e5e0a8 100644 --- a/src/frameworks/flutter/flutter-wizard-agent.ts +++ b/src/programs/frameworks/flutter/flutter-wizard-agent.ts @@ -1,10 +1,10 @@ /* Flutter wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import { Integration } from '@shared/constants'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { pubPackageManager } from '@lib/detection/package-manager'; +import { pubPackageManager } from '@programs/detection/package-manager'; /** Platform subtrees `flutter create` scaffolds; each needs its own setup notes. */ const FLUTTER_PLATFORM_DIRS = [ diff --git a/src/frameworks/go/go-wizard-agent.ts b/src/programs/frameworks/go/go-wizard-agent.ts similarity index 95% rename from src/frameworks/go/go-wizard-agent.ts rename to src/programs/frameworks/go/go-wizard-agent.ts index f9e630506..ae704da5b 100644 --- a/src/frameworks/go/go-wizard-agent.ts +++ b/src/programs/frameworks/go/go-wizard-agent.ts @@ -2,8 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { goModulesPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { goModulesPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; type GoContext = { diff --git a/src/frameworks/java/java-wizard-agent.ts b/src/programs/frameworks/java/java-wizard-agent.ts similarity index 97% rename from src/frameworks/java/java-wizard-agent.ts rename to src/programs/frameworks/java/java-wizard-agent.ts index 1c9e26828..8b27ab160 100644 --- a/src/frameworks/java/java-wizard-agent.ts +++ b/src/programs/frameworks/java/java-wizard-agent.ts @@ -3,8 +3,8 @@ import fg from 'fast-glob'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectJavaPackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectJavaPackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; type JavaContext = { diff --git a/src/frameworks/javascript-node/javascript-node-wizard-agent.ts b/src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts similarity index 94% rename from src/frameworks/javascript-node/javascript-node-wizard-agent.ts rename to src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts index 62519d721..4fc410be3 100644 --- a/src/frameworks/javascript-node/javascript-node-wizard-agent.ts +++ b/src/programs/frameworks/javascript-node/javascript-node-wizard-agent.ts @@ -1,8 +1,8 @@ /* Generic Node.js language wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '@lib/framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import { Integration } from '@shared/constants'; import { tryGetPackageJson } from '@utils/setup-utils'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; type JavaScriptNodeContext = Record; diff --git a/src/frameworks/javascript-web/javascript-web-wizard-agent.ts b/src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts similarity index 96% rename from src/frameworks/javascript-web/javascript-web-wizard-agent.ts rename to src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts index cc3899c06..1a662796c 100644 --- a/src/frameworks/javascript-web/javascript-web-wizard-agent.ts +++ b/src/programs/frameworks/javascript-web/javascript-web-wizard-agent.ts @@ -1,6 +1,6 @@ /* Generic JavaScript Web (client-side) wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import { Integration } from '@shared/constants'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -13,7 +13,7 @@ import { hasIndexHtml, type JavaScriptContext, } from './utils'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig = { metadata: { diff --git a/src/frameworks/javascript-web/utils.ts b/src/programs/frameworks/javascript-web/utils.ts similarity index 100% rename from src/frameworks/javascript-web/utils.ts rename to src/programs/frameworks/javascript-web/utils.ts diff --git a/src/frameworks/kmp/kmp-wizard-agent.ts b/src/programs/frameworks/kmp/kmp-wizard-agent.ts similarity index 96% rename from src/frameworks/kmp/kmp-wizard-agent.ts rename to src/programs/frameworks/kmp/kmp-wizard-agent.ts index be2a3e969..87328a0f4 100644 --- a/src/frameworks/kmp/kmp-wizard-agent.ts +++ b/src/programs/frameworks/kmp/kmp-wizard-agent.ts @@ -1,7 +1,7 @@ /* Kotlin Multiplatform (KMP) wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '../../lib/framework-config'; +import type { FrameworkConfig } from '../../framework-config'; import { Integration } from '@shared/constants'; -import { gradlePackageManager } from '../../lib/detection/package-manager'; +import { gradlePackageManager } from '../../detection/package-manager'; import { boundedGlob, readProjectFile } from '@utils/bounded-fs'; import * as path from 'node:path'; diff --git a/src/frameworks/laravel/laravel-wizard-agent.ts b/src/programs/frameworks/laravel/laravel-wizard-agent.ts similarity index 97% rename from src/frameworks/laravel/laravel-wizard-agent.ts rename to src/programs/frameworks/laravel/laravel-wizard-agent.ts index b7c0c1651..cc0849617 100644 --- a/src/frameworks/laravel/laravel-wizard-agent.ts +++ b/src/programs/frameworks/laravel/laravel-wizard-agent.ts @@ -1,7 +1,7 @@ /* Laravel wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { composerPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { composerPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import * as fs from 'node:fs'; diff --git a/src/frameworks/laravel/utils.ts b/src/programs/frameworks/laravel/utils.ts similarity index 100% rename from src/frameworks/laravel/utils.ts rename to src/programs/frameworks/laravel/utils.ts diff --git a/src/frameworks/nextjs/nextjs-wizard-agent.ts b/src/programs/frameworks/nextjs/nextjs-wizard-agent.ts similarity index 96% rename from src/frameworks/nextjs/nextjs-wizard-agent.ts rename to src/programs/frameworks/nextjs/nextjs-wizard-agent.ts index 614d9e7aa..83c180672 100644 --- a/src/frameworks/nextjs/nextjs-wizard-agent.ts +++ b/src/programs/frameworks/nextjs/nextjs-wizard-agent.ts @@ -1,7 +1,7 @@ /* Simplified Next.js wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/nextjs/utils.ts b/src/programs/frameworks/nextjs/utils.ts similarity index 100% rename from src/frameworks/nextjs/utils.ts rename to src/programs/frameworks/nextjs/utils.ts diff --git a/src/frameworks/nuxt/nuxt-wizard-agent.ts b/src/programs/frameworks/nuxt/nuxt-wizard-agent.ts similarity index 95% rename from src/frameworks/nuxt/nuxt-wizard-agent.ts rename to src/programs/frameworks/nuxt/nuxt-wizard-agent.ts index 94edb5173..7e197aca6 100644 --- a/src/frameworks/nuxt/nuxt-wizard-agent.ts +++ b/src/programs/frameworks/nuxt/nuxt-wizard-agent.ts @@ -1,7 +1,7 @@ /* Nuxt wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/python/python-wizard-agent.ts b/src/programs/frameworks/python/python-wizard-agent.ts similarity index 96% rename from src/frameworks/python/python-wizard-agent.ts rename to src/programs/frameworks/python/python-wizard-agent.ts index 15d88cd69..63fcf83aa 100644 --- a/src/frameworks/python/python-wizard-agent.ts +++ b/src/programs/frameworks/python/python-wizard-agent.ts @@ -1,8 +1,8 @@ /* Generic Python language wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { PYTHON_PACKAGE_INSTALLATION } from '@lib/framework-config'; -import { detectPythonPackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { PYTHON_PACKAGE_INSTALLATION } from '@programs/framework-config'; +import { detectPythonPackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import fg from 'fast-glob'; import * as fs from 'node:fs'; diff --git a/src/frameworks/python/utils.ts b/src/programs/frameworks/python/utils.ts similarity index 100% rename from src/frameworks/python/utils.ts rename to src/programs/frameworks/python/utils.ts diff --git a/src/frameworks/rails/rails-wizard-agent.ts b/src/programs/frameworks/rails/rails-wizard-agent.ts similarity index 96% rename from src/frameworks/rails/rails-wizard-agent.ts rename to src/programs/frameworks/rails/rails-wizard-agent.ts index e2df48c59..08c8c39a6 100644 --- a/src/frameworks/rails/rails-wizard-agent.ts +++ b/src/programs/frameworks/rails/rails-wizard-agent.ts @@ -1,7 +1,7 @@ /* Ruby on Rails wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { bundlerPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { bundlerPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getRailsVersion, diff --git a/src/frameworks/rails/utils.ts b/src/programs/frameworks/rails/utils.ts similarity index 100% rename from src/frameworks/rails/utils.ts rename to src/programs/frameworks/rails/utils.ts diff --git a/src/frameworks/react-native/react-native-wizard-agent.ts b/src/programs/frameworks/react-native/react-native-wizard-agent.ts similarity index 96% rename from src/frameworks/react-native/react-native-wizard-agent.ts rename to src/programs/frameworks/react-native/react-native-wizard-agent.ts index c4a489c71..2ba1b8f85 100644 --- a/src/frameworks/react-native/react-native-wizard-agent.ts +++ b/src/programs/frameworks/react-native/react-native-wizard-agent.ts @@ -1,7 +1,7 @@ /* React Native wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/react-native/utils.ts b/src/programs/frameworks/react-native/utils.ts similarity index 100% rename from src/frameworks/react-native/utils.ts rename to src/programs/frameworks/react-native/utils.ts diff --git a/src/frameworks/react-router/react-router-wizard-agent.ts b/src/programs/frameworks/react-router/react-router-wizard-agent.ts similarity index 96% rename from src/frameworks/react-router/react-router-wizard-agent.ts rename to src/programs/frameworks/react-router/react-router-wizard-agent.ts index 4c19a4ac4..b4dd9b38a 100644 --- a/src/frameworks/react-router/react-router-wizard-agent.ts +++ b/src/programs/frameworks/react-router/react-router-wizard-agent.ts @@ -1,7 +1,7 @@ /* React Router wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/react-router/utils.ts b/src/programs/frameworks/react-router/utils.ts similarity index 100% rename from src/frameworks/react-router/utils.ts rename to src/programs/frameworks/react-router/utils.ts diff --git a/src/frameworks/ruby/ruby-wizard-agent.ts b/src/programs/frameworks/ruby/ruby-wizard-agent.ts similarity index 96% rename from src/frameworks/ruby/ruby-wizard-agent.ts rename to src/programs/frameworks/ruby/ruby-wizard-agent.ts index 508ab33a5..c2907a536 100644 --- a/src/frameworks/ruby/ruby-wizard-agent.ts +++ b/src/programs/frameworks/ruby/ruby-wizard-agent.ts @@ -1,7 +1,7 @@ /* Generic Ruby language wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { bundlerPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { bundlerPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getRubyVersion, diff --git a/src/frameworks/ruby/utils.ts b/src/programs/frameworks/ruby/utils.ts similarity index 100% rename from src/frameworks/ruby/utils.ts rename to src/programs/frameworks/ruby/utils.ts diff --git a/src/frameworks/rust/rust-wizard-agent.ts b/src/programs/frameworks/rust/rust-wizard-agent.ts similarity index 96% rename from src/frameworks/rust/rust-wizard-agent.ts rename to src/programs/frameworks/rust/rust-wizard-agent.ts index 3113a6708..a1dd2711d 100644 --- a/src/frameworks/rust/rust-wizard-agent.ts +++ b/src/programs/frameworks/rust/rust-wizard-agent.ts @@ -2,8 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { cargoPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { cargoPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; type RustContext = { diff --git a/src/frameworks/svelte/svelte-wizard-agent.ts b/src/programs/frameworks/svelte/svelte-wizard-agent.ts similarity index 93% rename from src/frameworks/svelte/svelte-wizard-agent.ts rename to src/programs/frameworks/svelte/svelte-wizard-agent.ts index 5d7777fdc..1c5088c3c 100644 --- a/src/frameworks/svelte/svelte-wizard-agent.ts +++ b/src/programs/frameworks/svelte/svelte-wizard-agent.ts @@ -1,6 +1,6 @@ /* SvelteKit wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/swift/__tests__/utils.test.ts b/src/programs/frameworks/swift/__tests__/utils.test.ts similarity index 100% rename from src/frameworks/swift/__tests__/utils.test.ts rename to src/programs/frameworks/swift/__tests__/utils.test.ts diff --git a/src/frameworks/swift/swift-wizard-agent.ts b/src/programs/frameworks/swift/swift-wizard-agent.ts similarity index 96% rename from src/frameworks/swift/swift-wizard-agent.ts rename to src/programs/frameworks/swift/swift-wizard-agent.ts index 049255b64..58dd25782 100644 --- a/src/frameworks/swift/swift-wizard-agent.ts +++ b/src/programs/frameworks/swift/swift-wizard-agent.ts @@ -1,7 +1,7 @@ /* Swift wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { swiftPackageManager } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { swiftPackageManager } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { boundedGlob } from '@utils/bounded-fs'; import fg from 'fast-glob'; diff --git a/src/frameworks/swift/utils.ts b/src/programs/frameworks/swift/utils.ts similarity index 100% rename from src/frameworks/swift/utils.ts rename to src/programs/frameworks/swift/utils.ts diff --git a/src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts b/src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts similarity index 96% rename from src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts rename to src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts index 07e43d0ad..d93e337bf 100644 --- a/src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts +++ b/src/programs/frameworks/tanstack-router/tanstack-router-wizard-agent.ts @@ -1,7 +1,7 @@ /* TanStack Router wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/tanstack-router/utils.ts b/src/programs/frameworks/tanstack-router/utils.ts similarity index 100% rename from src/frameworks/tanstack-router/utils.ts rename to src/programs/frameworks/tanstack-router/utils.ts diff --git a/src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts b/src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts similarity index 95% rename from src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts rename to src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts index f28eaa4a9..fcb52a0d6 100644 --- a/src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts +++ b/src/programs/frameworks/tanstack-start/tanstack-start-wizard-agent.ts @@ -1,7 +1,7 @@ /* TanStack Start wizard using posthog-agent with PostHog MCP */ import type { WizardRunOptions } from '@utils/types'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/frameworks/tanstack-start/utils.ts b/src/programs/frameworks/tanstack-start/utils.ts similarity index 100% rename from src/frameworks/tanstack-start/utils.ts rename to src/programs/frameworks/tanstack-start/utils.ts diff --git a/src/frameworks/vue/vue-wizard-agent.ts b/src/programs/frameworks/vue/vue-wizard-agent.ts similarity index 93% rename from src/frameworks/vue/vue-wizard-agent.ts rename to src/programs/frameworks/vue/vue-wizard-agent.ts index dd61b3ab4..0f2ae5cee 100644 --- a/src/frameworks/vue/vue-wizard-agent.ts +++ b/src/programs/frameworks/vue/vue-wizard-agent.ts @@ -1,6 +1,6 @@ /* Vue wizard using posthog-agent with PostHog MCP */ -import type { FrameworkConfig } from '@lib/framework-config'; -import { detectNodePackageManagers } from '@lib/detection/package-manager'; +import type { FrameworkConfig } from '@programs/framework-config'; +import { detectNodePackageManagers } from '@programs/detection/package-manager'; import { Integration } from '@shared/constants'; import { getDeclaredVersion, diff --git a/src/programs/index.ts b/src/programs/index.ts new file mode 100644 index 000000000..68a7e3d1e --- /dev/null +++ b/src/programs/index.ts @@ -0,0 +1,10 @@ +/** Public runtime entry for the programs surface. */ +export type * from './types'; +export { + Program, + PROGRAM_REGISTRY, + getProgramConfig, + getSubcommandPrograms, + getCommandPath, + getLaunchablePrograms, +} from './program-registry'; diff --git a/src/lib/programs/mcp-analytics/index.ts b/src/programs/mcp-analytics/index.ts similarity index 98% rename from src/lib/programs/mcp-analytics/index.ts rename to src/programs/mcp-analytics/index.ts index 458ba18e7..faa9afd1c 100644 --- a/src/lib/programs/mcp-analytics/index.ts +++ b/src/programs/mcp-analytics/index.ts @@ -1,6 +1,6 @@ import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; -import { createSkillProgram } from '@lib/programs/agent-skill/index'; +import { createSkillProgram } from '@programs/agent-skill/index'; const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; diff --git a/src/lib/programs/mcp/index.ts b/src/programs/mcp/index.ts similarity index 98% rename from src/lib/programs/mcp/index.ts rename to src/programs/mcp/index.ts index 819e19e8e..5c0e35ce5 100644 --- a/src/lib/programs/mcp/index.ts +++ b/src/programs/mcp/index.ts @@ -8,7 +8,7 @@ * screen-sequences.ts). */ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; import { McpOutcome } from '@lib/wizard-session'; export const mcpAddConfig: ProgramConfig = { diff --git a/src/lib/programs/metrics/index.ts b/src/programs/metrics/index.ts similarity index 93% rename from src/lib/programs/metrics/index.ts rename to src/programs/metrics/index.ts index 859eb6d19..c96612ae3 100644 --- a/src/lib/programs/metrics/index.ts +++ b/src/programs/metrics/index.ts @@ -1,6 +1,6 @@ -import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; -import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/index'; -import { getContentBlocks } from '@lib/programs/agent-skill/content/index'; +import type { ProgramConfig, ProgramStep } from '@programs/program-step'; +import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; +import { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; const METRICS_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => step.id === 'intro' ? { ...step, screenId: 'metrics-intro' } : step, diff --git a/src/lib/programs/metrics/test/e2e.json b/src/programs/metrics/test/e2e.json similarity index 100% rename from src/lib/programs/metrics/test/e2e.json rename to src/programs/metrics/test/e2e.json diff --git a/src/lib/programs/migration/index.ts b/src/programs/migration/index.ts similarity index 94% rename from src/lib/programs/migration/index.ts rename to src/programs/migration/index.ts index 173e007be..907160f6d 100644 --- a/src/lib/programs/migration/index.ts +++ b/src/programs/migration/index.ts @@ -1,8 +1,8 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; import type { AbortCase } from '@agent/types'; import { WIZARD_TOOL_NAMES } from '@agent'; import { MIGRATION_PROGRAM } from './steps.js'; -import { getContentBlocks } from './content/index.js'; +import { getContentBlocks } from '../../ui/tui/decks/migration/index.js'; const MIGRATION_REPORT_FILE = 'migration-report.md'; diff --git a/src/lib/programs/migration/steps.ts b/src/programs/migration/steps.ts similarity index 84% rename from src/lib/programs/migration/steps.ts rename to src/programs/migration/steps.ts index 33635d863..cb41d5bc0 100644 --- a/src/lib/programs/migration/steps.ts +++ b/src/programs/migration/steps.ts @@ -1,6 +1,6 @@ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import { RunPhase } from '@lib/wizard-session'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; export const MIGRATION_PROGRAM: ProgramStep[] = [ { diff --git a/src/lib/oauth/__tests__/program-scopes.test.ts b/src/programs/oauth/__tests__/program-scopes.test.ts similarity index 98% rename from src/lib/oauth/__tests__/program-scopes.test.ts rename to src/programs/oauth/__tests__/program-scopes.test.ts index 773a7f15d..1200b444b 100644 --- a/src/lib/oauth/__tests__/program-scopes.test.ts +++ b/src/programs/oauth/__tests__/program-scopes.test.ts @@ -6,7 +6,7 @@ import { getOAuthScopesForProgram, getProvisioningScopesForProgram, -} from '@lib/oauth/program-scopes'; +} from '@programs/oauth/program-scopes'; describe('posthog-integration scopes', () => { it('includes the warehouse pair for the orchestrator warehouse task', () => { diff --git a/src/lib/oauth/program-scopes.ts b/src/programs/oauth/program-scopes.ts similarity index 99% rename from src/lib/oauth/program-scopes.ts rename to src/programs/oauth/program-scopes.ts index 37ade34e5..e3c5450df 100644 --- a/src/lib/oauth/program-scopes.ts +++ b/src/programs/oauth/program-scopes.ts @@ -31,7 +31,7 @@ // read as `undefined` at module init. Keep this type-only and reference // program IDs by their string-literal value below — TypeScript still // catches renames via the `Partial>` keying. -import type { ProgramId } from '@lib/programs/program-registry'; +import type { ProgramId } from '@programs/program-registry'; import { WIZARD_OAUTH_SCOPES, WIZARD_PROVISIONING_SCOPES, diff --git a/src/lib/programs/posthog-doctor/__tests__/doctor-schema.test.ts b/src/programs/posthog-doctor/__tests__/doctor-schema.test.ts similarity index 95% rename from src/lib/programs/posthog-doctor/__tests__/doctor-schema.test.ts rename to src/programs/posthog-doctor/__tests__/doctor-schema.test.ts index cc23b479f..9fb87640c 100644 --- a/src/lib/programs/posthog-doctor/__tests__/doctor-schema.test.ts +++ b/src/programs/posthog-doctor/__tests__/doctor-schema.test.ts @@ -1,12 +1,12 @@ import { HealthIssueListResponseSchema, HealthIssueSchema, -} from '@lib/programs/posthog-doctor/types'; +} from '@programs/posthog-doctor/types'; import { getKindMeta, KIND_METADATA, UNKNOWN_KIND_META, -} from '@lib/programs/posthog-doctor/kind-metadata'; +} from '@programs/posthog-doctor/kind-metadata'; describe('posthog-doctor schema', () => { const canned = { diff --git a/src/lib/programs/posthog-doctor/fetch.ts b/src/programs/posthog-doctor/fetch.ts similarity index 100% rename from src/lib/programs/posthog-doctor/fetch.ts rename to src/programs/posthog-doctor/fetch.ts diff --git a/src/lib/programs/posthog-doctor/index.ts b/src/programs/posthog-doctor/index.ts similarity index 91% rename from src/lib/programs/posthog-doctor/index.ts rename to src/programs/posthog-doctor/index.ts index a6ad9f10e..f5a7c5f96 100644 --- a/src/lib/programs/posthog-doctor/index.ts +++ b/src/programs/posthog-doctor/index.ts @@ -1,4 +1,4 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; import { WIZARD_TOOL_NAMES } from '@agent'; import { POSTHOG_DOCTOR_PROGRAM } from './steps.js'; diff --git a/src/lib/programs/posthog-doctor/kind-metadata.ts b/src/programs/posthog-doctor/kind-metadata.ts similarity index 100% rename from src/lib/programs/posthog-doctor/kind-metadata.ts rename to src/programs/posthog-doctor/kind-metadata.ts diff --git a/src/lib/programs/posthog-doctor/steps.ts b/src/programs/posthog-doctor/steps.ts similarity index 81% rename from src/lib/programs/posthog-doctor/steps.ts rename to src/programs/posthog-doctor/steps.ts index fe0ef895c..3a97bc42a 100644 --- a/src/lib/programs/posthog-doctor/steps.ts +++ b/src/programs/posthog-doctor/steps.ts @@ -1,5 +1,5 @@ -import type { ProgramStep } from '@lib/programs/program-step'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; +import type { ProgramStep } from '@programs/program-step'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; export const POSTHOG_DOCTOR_PROGRAM: ProgramStep[] = [ { diff --git a/src/lib/programs/posthog-doctor/types.ts b/src/programs/posthog-doctor/types.ts similarity index 100% rename from src/lib/programs/posthog-doctor/types.ts rename to src/programs/posthog-doctor/types.ts diff --git a/src/lib/programs/posthog-integration/__tests__/detect.test.ts b/src/programs/posthog-integration/__tests__/detect.test.ts similarity index 96% rename from src/lib/programs/posthog-integration/__tests__/detect.test.ts rename to src/programs/posthog-integration/__tests__/detect.test.ts index 3e4a67f56..3984a2c61 100644 --- a/src/lib/programs/posthog-integration/__tests__/detect.test.ts +++ b/src/programs/posthog-integration/__tests__/detect.test.ts @@ -15,9 +15,9 @@ vi.mock('@utils/analytics', () => ({ })); // Wrapped so one test can force a throw; the rest use the real scanner. -vi.mock('@lib/warehouse-sources/detect', async (importOriginal) => { +vi.mock('@programs/warehouse-sources/detect', async (importOriginal) => { const actual = await importOriginal< - typeof import('@lib/warehouse-sources/detect') + typeof import('@programs/warehouse-sources/detect') >(); return { ...actual, @@ -26,22 +26,22 @@ vi.mock('@lib/warehouse-sources/detect', async (importOriginal) => { }); import { analytics } from '@utils/analytics'; -import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; +import { detectWarehouseSources } from '@programs/warehouse-sources/detect'; import { detectPostHogIntegration, maybeStampAiSdkDetected, reportWarehouseSourcesDetected, -} from '@lib/programs/posthog-integration/detect'; -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; -import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@lib/programs/warehouse-source/detect'; -import type { ProgramReadyContext } from '@lib/programs/program-step'; +} from '@programs/posthog-integration/detect'; +import { posthogIntegrationConfig } from '@programs/posthog-integration/index'; +import { DETECTED_WAREHOUSE_SOURCES_KEY } from '@programs/warehouse-source/detect'; +import type { ProgramReadyContext } from '@programs/types'; import { buildSession, DiscoveredFeature, ScanConsent, type WizardSession, } from '@lib/wizard-session'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'warehouse-reporting-')); @@ -288,7 +288,7 @@ describe('reportWarehouseSourcesDetected', () => { // scanned". const session = buildSession({ installDir: tmpDir, ci: true }); const { detectWarehousePrerequisites } = await import( - '@lib/programs/warehouse-source/detect' + '@programs/warehouse-source/detect' ); detectWarehousePrerequisites(session, (key, value) => { session.frameworkContext[key] = value; diff --git a/src/lib/programs/posthog-integration/__tests__/handoff.test.ts b/src/programs/posthog-integration/__tests__/handoff.test.ts similarity index 100% rename from src/lib/programs/posthog-integration/__tests__/handoff.test.ts rename to src/programs/posthog-integration/__tests__/handoff.test.ts diff --git a/src/lib/programs/posthog-integration/__tests__/index.test.ts b/src/programs/posthog-integration/__tests__/index.test.ts similarity index 96% rename from src/lib/programs/posthog-integration/__tests__/index.test.ts rename to src/programs/posthog-integration/__tests__/index.test.ts index ec55f6efe..88cbee0cf 100644 --- a/src/lib/programs/posthog-integration/__tests__/index.test.ts +++ b/src/programs/posthog-integration/__tests__/index.test.ts @@ -6,7 +6,7 @@ * state, so the value rides on every later capture either way. */ -import { posthogIntegrationConfig } from '@lib/programs/posthog-integration/index'; +import { posthogIntegrationConfig } from '@programs/posthog-integration/index'; import { buildSession, type WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { isUsingTypeScript } from '@utils/setup-utils'; diff --git a/src/lib/programs/posthog-integration/constants.ts b/src/programs/posthog-integration/constants.ts similarity index 100% rename from src/lib/programs/posthog-integration/constants.ts rename to src/programs/posthog-integration/constants.ts diff --git a/src/lib/programs/posthog-integration/detect.ts b/src/programs/posthog-integration/detect.ts similarity index 95% rename from src/lib/programs/posthog-integration/detect.ts rename to src/programs/posthog-integration/detect.ts index f27faa6a9..ab4effb72 100644 --- a/src/lib/programs/posthog-integration/detect.ts +++ b/src/programs/posthog-integration/detect.ts @@ -9,29 +9,29 @@ * extracted here so the `integrate` subcommand can reuse it. */ -import type { ProgramReadyContext } from '@lib/programs/program-step'; +import type { ProgramReadyContext } from '@programs/program-step'; import { DiscoveredFeature, mayReportScanResults, ScanConsent, type WizardSession, } from '@lib/wizard-session'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { detectFramework, discoverFeatures, gatherFrameworkContext, checkFrameworkVersion, -} from '@lib/detection/index'; +} from '@programs/detection/index'; import { analytics } from '@utils/analytics'; -import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; -import { AI_SOURCE_KINDS } from '@lib/warehouse-sources/registry'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import { detectWarehouseSources } from '@programs/warehouse-sources/detect'; +import { AI_SOURCE_KINDS } from '@programs/warehouse-sources/registry'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; import { DETECTED_WAREHOUSE_SOURCES_KEY, getDetectedWarehouseSources, -} from '@lib/programs/warehouse-source/detect'; -import { findPackageJsons } from '@lib/programs/shared/package-scanning'; +} from '@programs/warehouse-source/detect'; +import { findPackageJsons } from '@programs/shared/package-scanning'; export async function detectPostHogIntegration( ctx: ProgramReadyContext, diff --git a/src/lib/programs/posthog-integration/handoff.ts b/src/programs/posthog-integration/handoff.ts similarity index 100% rename from src/lib/programs/posthog-integration/handoff.ts rename to src/programs/posthog-integration/handoff.ts diff --git a/src/lib/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts similarity index 97% rename from src/lib/programs/posthog-integration/index.ts rename to src/programs/posthog-integration/index.ts index 4a6a04a2b..a2e1f037f 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -1,18 +1,21 @@ -import type { ProgramConfig, ProgramStep } from '@lib/programs/program-step'; -import { runProgramAgent } from '@lib/programs/run-agent-legacy'; -import type { ProgramRun } from '@lib/programs/program-run'; +import type { ProgramConfig, ProgramStep } from '@programs/program-step'; +import { runProgramAgent } from '@programs/run-agent-legacy'; +import type { ProgramRun } from '@programs/program-run'; import { AgentSignals, shouldDisableAsk, WIZARD_TOOL_NAMES } from '@agent'; import type { WizardSession } from '@lib/wizard-session'; import { mayReportScanResults, OutroKind, RunPhase } from '@lib/wizard-session'; import { DEFAULT_PACKAGE_INSTALLATION, SPINNER_MESSAGE, -} from '@lib/framework-config'; +} from '@programs/framework-config'; import { tryGetPackageJson, isUsingTypeScript } from '@utils/setup-utils'; import { analytics } from '@utils/analytics'; -import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; -import { scopeInstallDirToProject } from '@lib/detection/project-scope'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { + detectFramework, + gatherFrameworkContext, +} from '@programs/detection/index'; +import { scopeInstallDirToProject } from '@programs/detection/project-scope'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; import { WIZARD_INTERACTION_EVENT_NAME } from '@shared/constants'; @@ -20,9 +23,9 @@ import { getUI } from '@ui/index'; import { requestDeepLink } from '@utils/provisioning'; import { openTrackedLink, withUtm } from '@utils/links'; import type { HostResolution } from '@shared/host-resolution'; -import { getDetectedWarehouseSources } from '@lib/programs/warehouse-source/detect'; +import { getDetectedWarehouseSources } from '@programs/warehouse-source/detect'; import { POSTHOG_INTEGRATION_PROGRAM } from './steps.js'; -import { getContentBlocks } from './content/index.js'; +import { getContentBlocks } from '../../ui/tui/decks/posthog-integration/index.js'; import { buildCodingAgentPrompt } from './handoff.js'; import { EVENT_PLAN_FILE } from './constants.js'; diff --git a/src/lib/programs/posthog-integration/steps.ts b/src/programs/posthog-integration/steps.ts similarity index 94% rename from src/lib/programs/posthog-integration/steps.ts rename to src/programs/posthog-integration/steps.ts index bc5adc3b0..3c4e9dc5b 100644 --- a/src/lib/programs/posthog-integration/steps.ts +++ b/src/programs/posthog-integration/steps.ts @@ -6,10 +6,10 @@ * definitions — no hardcoded per-flow logic in the store. */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import type { WizardSession } from '@lib/wizard-session'; import { RunPhase } from '@lib/wizard-session'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; import { detectPostHogIntegration } from './detect.js'; function needsSetup(session: WizardSession): boolean { diff --git a/src/lib/programs/posthog-integration/test/README.md b/src/programs/posthog-integration/test/README.md similarity index 90% rename from src/lib/programs/posthog-integration/test/README.md rename to src/programs/posthog-integration/test/README.md index 34a008a65..211f6c3dc 100644 --- a/src/lib/programs/posthog-integration/test/README.md +++ b/src/programs/posthog-integration/test/README.md @@ -6,7 +6,7 @@ documented `path` of every screen and what it does. - **`profile`** — the machine-read part. The harness loads it via `profileFor(Program.PostHogIntegration)` - ([`e2e-harness/profiles.ts`](../../../../../e2e-harness/profiles.ts)) and asks + ([`e2e-harness/profiles.ts`](../../../../e2e-harness/profiles.ts)) and asks `decideE2eAction` what to commit on each screen. - **`path`** — the human-read part: each screen in order and the auto-decision, so you can see the whole walk at a glance. diff --git a/src/lib/programs/posthog-integration/test/e2e.json b/src/programs/posthog-integration/test/e2e.json similarity index 100% rename from src/lib/programs/posthog-integration/test/e2e.json rename to src/programs/posthog-integration/test/e2e.json diff --git a/src/lib/programs/program-registry.ts b/src/programs/program-registry.ts similarity index 97% rename from src/lib/programs/program-registry.ts rename to src/programs/program-registry.ts index 2b001e56c..be63b4b8a 100644 --- a/src/lib/programs/program-registry.ts +++ b/src/programs/program-registry.ts @@ -2,7 +2,7 @@ * Central registry of all wizard programs. * * Adding a new program: - * 1. Create src/lib/programs// with index.ts exporting a ProgramConfig + * 1. Create src/programs// with index.ts exporting a ProgramConfig * 2. Import and add it to PROGRAM_REGISTRY below * 3. (If custom intro screen) add to src/ui/tui/screen-registry.tsx * @@ -24,7 +24,7 @@ import { errorTrackingUploadSourceMapsConfig } from './error-tracking-upload-sou import { errorTrackingConfig } from './error-tracking/index.js'; import { selfDrivingConfig } from './self-driving/index.js'; import { AGENT_SKILL_STEPS } from './agent-skill/index.js'; -import { getContentBlocks as agentSkillContentBlocks } from './agent-skill/content/index.js'; +import { getContentBlocks as agentSkillContentBlocks } from '../ui/tui/decks/agent-skill/index.js'; import { mcpAddConfig, mcpRemoveConfig, diff --git a/src/lib/programs/program-run.ts b/src/programs/program-run.ts similarity index 100% rename from src/lib/programs/program-run.ts rename to src/programs/program-run.ts diff --git a/src/lib/programs/program-step.ts b/src/programs/program-step.ts similarity index 99% rename from src/lib/programs/program-step.ts rename to src/programs/program-step.ts index f0983c9e0..70fbf2e18 100644 --- a/src/lib/programs/program-step.ts +++ b/src/programs/program-step.ts @@ -4,9 +4,9 @@ import type { TaskNotice, } from '@lib/wizard-session'; import type { WizardReadinessResult } from '@shared/health-checks/readiness'; -import type { ProgramRun } from '@lib/programs/program-run'; +import type { ProgramRun } from '@programs/program-run'; import type { Integration } from '@shared/constants'; -import type { FrameworkConfig } from '@lib/framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import type { ContentBlock } from '@ui/tui/primitives/index'; import type { WizardStore } from '@ui/tui/store'; import type { Tip } from '@ui/tui/components/TipsCard'; diff --git a/src/programs/registry.ts b/src/programs/registry.ts new file mode 100644 index 000000000..649dc3a99 --- /dev/null +++ b/src/programs/registry.ts @@ -0,0 +1,59 @@ +import type { FrameworkConfig } from './framework-config'; +import { Integration } from '@shared/constants'; +import { NEXTJS_AGENT_CONFIG } from '@programs/frameworks/nextjs/nextjs-wizard-agent'; +import { NUXT_AGENT_CONFIG } from '@programs/frameworks/nuxt/nuxt-wizard-agent'; +import { VUE_AGENT_CONFIG } from '@programs/frameworks/vue/vue-wizard-agent'; +import { REACT_ROUTER_AGENT_CONFIG } from '@programs/frameworks/react-router/react-router-wizard-agent'; +import { TANSTACK_ROUTER_AGENT_CONFIG } from '@programs/frameworks/tanstack-router/tanstack-router-wizard-agent'; +import { TANSTACK_START_AGENT_CONFIG } from '@programs/frameworks/tanstack-start/tanstack-start-wizard-agent'; +import { REACT_NATIVE_AGENT_CONFIG } from '@programs/frameworks/react-native/react-native-wizard-agent'; +import { ANGULAR_AGENT_CONFIG } from '@programs/frameworks/angular/angular-wizard-agent'; +import { ASTRO_AGENT_CONFIG } from '@programs/frameworks/astro/astro-wizard-agent'; +import { DJANGO_AGENT_CONFIG } from '@programs/frameworks/django/django-wizard-agent'; +import { FLASK_AGENT_CONFIG } from '@programs/frameworks/flask/flask-wizard-agent'; +import { FASTAPI_AGENT_CONFIG } from '@programs/frameworks/fastapi/fastapi-wizard-agent'; +import { LARAVEL_AGENT_CONFIG } from '@programs/frameworks/laravel/laravel-wizard-agent'; +import { SVELTEKIT_AGENT_CONFIG } from '@programs/frameworks/svelte/svelte-wizard-agent'; +import { FLUTTER_AGENT_CONFIG } from '@programs/frameworks/flutter/flutter-wizard-agent'; +import { SWIFT_AGENT_CONFIG } from '@programs/frameworks/swift/swift-wizard-agent'; +import { KMP_AGENT_CONFIG } from '@programs/frameworks/kmp/kmp-wizard-agent'; +import { ANDROID_AGENT_CONFIG } from '@programs/frameworks/android/android-wizard-agent'; +import { RAILS_AGENT_CONFIG } from '@programs/frameworks/rails/rails-wizard-agent'; +import { ELIXIR_AGENT_CONFIG } from '@programs/frameworks/elixir/elixir-wizard-agent'; +import { GO_AGENT_CONFIG } from '@programs/frameworks/go/go-wizard-agent'; +import { JAVA_AGENT_CONFIG } from '@programs/frameworks/java/java-wizard-agent'; +import { RUST_AGENT_CONFIG } from '@programs/frameworks/rust/rust-wizard-agent'; +import { PYTHON_AGENT_CONFIG } from '@programs/frameworks/python/python-wizard-agent'; +import { RUBY_AGENT_CONFIG } from '@programs/frameworks/ruby/ruby-wizard-agent'; +import { JAVASCRIPT_NODE_AGENT_CONFIG } from '@programs/frameworks/javascript-node/javascript-node-wizard-agent'; +import { JAVASCRIPT_WEB_AGENT_CONFIG } from '@programs/frameworks/javascript-web/javascript-web-wizard-agent'; + +export const FRAMEWORK_REGISTRY: Record = { + [Integration.nextjs]: NEXTJS_AGENT_CONFIG, + [Integration.nuxt]: NUXT_AGENT_CONFIG, + [Integration.vue]: VUE_AGENT_CONFIG, + [Integration.tanstackStart]: TANSTACK_START_AGENT_CONFIG, + [Integration.reactRouter]: REACT_ROUTER_AGENT_CONFIG, + [Integration.tanstackRouter]: TANSTACK_ROUTER_AGENT_CONFIG, + [Integration.reactNative]: REACT_NATIVE_AGENT_CONFIG, + [Integration.angular]: ANGULAR_AGENT_CONFIG, + [Integration.astro]: ASTRO_AGENT_CONFIG, + [Integration.django]: DJANGO_AGENT_CONFIG, + [Integration.flask]: FLASK_AGENT_CONFIG, + [Integration.fastapi]: FASTAPI_AGENT_CONFIG, + [Integration.laravel]: LARAVEL_AGENT_CONFIG, + [Integration.sveltekit]: SVELTEKIT_AGENT_CONFIG, + [Integration.flutter]: FLUTTER_AGENT_CONFIG, + [Integration.kmp]: KMP_AGENT_CONFIG, + [Integration.swift]: SWIFT_AGENT_CONFIG, + [Integration.android]: ANDROID_AGENT_CONFIG, + [Integration.rails]: RAILS_AGENT_CONFIG, + [Integration.elixir]: ELIXIR_AGENT_CONFIG, + [Integration.go]: GO_AGENT_CONFIG, + [Integration.rust]: RUST_AGENT_CONFIG, + [Integration.java]: JAVA_AGENT_CONFIG, + [Integration.python]: PYTHON_AGENT_CONFIG, + [Integration.ruby]: RUBY_AGENT_CONFIG, + [Integration.javascriptNode]: JAVASCRIPT_NODE_AGENT_CONFIG, + [Integration.javascript_web]: JAVASCRIPT_WEB_AGENT_CONFIG, +}; diff --git a/src/lib/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts similarity index 94% rename from src/lib/programs/replay-vision/index.ts rename to src/programs/replay-vision/index.ts index ef01e6a9d..314bfc9a4 100644 --- a/src/lib/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -1,16 +1,19 @@ import type { AbortCase } from '@agent/types'; import { Integration } from '@shared/constants'; -import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; -import { scopeInstallDirToProject } from '@lib/detection/project-scope'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { createSkillProgram } from '@lib/programs/agent-skill/index'; -import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/steps'; -import { detectPostHogIntegration } from '@lib/programs/posthog-integration/detect'; +import { + detectFramework, + gatherFrameworkContext, +} from '@programs/detection/index'; +import { scopeInstallDirToProject } from '@programs/detection/project-scope'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { createSkillProgram } from '@programs/agent-skill/index'; +import { AGENT_SKILL_STEPS } from '@programs/agent-skill/steps'; +import { detectPostHogIntegration } from '@programs/posthog-integration/detect'; import type { ProgramConfig, ProgramReadyContext, ProgramStep, -} from '@lib/programs/program-step'; +} from '@programs/program-step'; import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; diff --git a/src/lib/programs/replay-vision/test/e2e.json b/src/programs/replay-vision/test/e2e.json similarity index 100% rename from src/lib/programs/replay-vision/test/e2e.json rename to src/programs/replay-vision/test/e2e.json diff --git a/src/lib/programs/revenue-analytics/detect.ts b/src/programs/revenue-analytics/detect.ts similarity index 97% rename from src/lib/programs/revenue-analytics/detect.ts rename to src/programs/revenue-analytics/detect.ts index d63ef3c6a..b81090ffd 100644 --- a/src/lib/programs/revenue-analytics/detect.ts +++ b/src/programs/revenue-analytics/detect.ts @@ -8,14 +8,14 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; import type { AbortCase } from '@agent/types'; -import { findPackageJsons } from '@lib/programs/shared/package-scanning'; +import { findPackageJsons } from '@programs/shared/package-scanning'; export { findPackageJsons, POSTHOG_SDKS, STRIPE_SDKS, type PackageMatch, -} from '@lib/programs/shared/package-scanning'; +} from '@programs/shared/package-scanning'; /** * Structured detection errors. The screen renders each kind into JSX diff --git a/src/lib/programs/revenue-analytics/index.ts b/src/programs/revenue-analytics/index.ts similarity index 89% rename from src/lib/programs/revenue-analytics/index.ts rename to src/programs/revenue-analytics/index.ts index fade6e3fc..4fae357ac 100644 --- a/src/lib/programs/revenue-analytics/index.ts +++ b/src/programs/revenue-analytics/index.ts @@ -1,8 +1,8 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; import { WIZARD_TOOL_NAMES } from '@agent'; import { REVENUE_ANALYTICS_PROGRAM } from './steps.js'; import { REVENUE_ABORT_CASES } from './detect.js'; -import { getContentBlocks } from './content/index.js'; +import { getContentBlocks } from '../../ui/tui/decks/revenue-analytics/index.js'; export const revenueAnalyticsConfig: ProgramConfig = { command: 'revenue-analytics', diff --git a/src/lib/programs/revenue-analytics/steps.ts b/src/programs/revenue-analytics/steps.ts similarity index 91% rename from src/lib/programs/revenue-analytics/steps.ts rename to src/programs/revenue-analytics/steps.ts index 7e983ab9d..b53926536 100644 --- a/src/lib/programs/revenue-analytics/steps.ts +++ b/src/programs/revenue-analytics/steps.ts @@ -5,9 +5,9 @@ * and agent run live in the program runner (see agent-runner.ts). */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import { RunPhase } from '@lib/wizard-session'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; import { detectRevenuePrerequisites } from './detect.js'; export const REVENUE_ANALYTICS_PROGRAM: ProgramStep[] = [ diff --git a/src/lib/programs/run-agent-legacy.ts b/src/programs/run-agent-legacy.ts similarity index 99% rename from src/lib/programs/run-agent-legacy.ts rename to src/programs/run-agent-legacy.ts index e0c0733d0..86f6c1e3b 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/programs/run-agent-legacy.ts @@ -55,7 +55,7 @@ import { WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, type Integration, } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { postAuthGateSteps, type ProgramConfig } from './program-step'; import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; import { maybeStampAiSdkDetected } from './posthog-integration/detect'; diff --git a/src/lib/programs/self-driving/ARCHITECTURE.md b/src/programs/self-driving/ARCHITECTURE.md similarity index 98% rename from src/lib/programs/self-driving/ARCHITECTURE.md rename to src/programs/self-driving/ARCHITECTURE.md index 39f4f8cbf..846648869 100644 --- a/src/lib/programs/self-driving/ARCHITECTURE.md +++ b/src/programs/self-driving/ARCHITECTURE.md @@ -22,11 +22,11 @@ anchors are point-in-time — the symbol names are the durable part. | Need | Go to | | -------------------------------------- | ---------------------------------------------------------- | -| The ordered steps | `src/lib/programs/self-driving/prompt.ts` | +| The ordered steps | `src/programs/self-driving/prompt.ts` | | What each step _does_ | `context-mill/context/skills/self-driving/references/*.md` | -| Program registration / lifecycle | `src/lib/programs/self-driving/index.ts` | +| Program registration / lifecycle | `src/programs/self-driving/index.ts` | | `wizard_ask` / `.env` tools | `src/agent/tools/tools.ts`, `src/agent/wizard-ask-bridge.ts` | -| OAuth scopes (+ prod ceiling) | `src/lib/oauth/program-scopes.ts` (§3, §7) | +| OAuth scopes (+ prod ceiling) | `src/programs/oauth/program-scopes.ts` (§3, §7) | | Signals models / MCP / sync | `posthog/products/signals/backend/…` (§5) | | Why a team gets no findings | §6 | | What to change for prod | §7 | @@ -146,16 +146,16 @@ repos. ## 3. Wizard internals -**Program definition** (`src/lib/programs/self-driving/`, five files): +**Program definition** (`src/programs/self-driving/`, four core files): `index.ts` (config + lifecycle), `prompt.ts` (the 10 steps + mechanics + project URLs), `detect.ts` (prerequisite check + abort vocabulary), `steps.ts` (TUI screen sequence `detect → intro → health-check → auth → self-driving-github → -run → outro`), and -`content/tips.ts` (the program-owned `Tips`-sidebar copy that defines signal +run → outro`). The TUI deck at +`src/ui/tui/decks/self-driving/tips.ts` (the `Tips`-sidebar copy that defines signal sources + scouts + scanners in plain language, wired via `getTips`; `RunScreen` falls back to `DEFAULT_TIPS` for every other program, so nothing else is affected). `selfDrivingConfig` is built from the `createSkillProgram` factory -(`src/lib/programs/agent-skill/`) with overrides. Notables in `index.ts`: +(`src/programs/agent-skill/`) with overrides. Notables in `index.ts`: `SELF_DRIVING_SKILL_ID = 'self-driving-setup'`, `REPORT_FILE = 'posthog-self-driving-report.md'`, `maxQuestions: 13` (tracker picks + custom-scout proposal), `richLinks: true` (OSC-8 links so long @@ -200,7 +200,7 @@ returns an error telling the agent to default or emit brokers into the TUI overlay; cancelled/timed-out fields resolve to `CANCELLED_SENTINEL = '__cancelled__'`. -**OAuth scopes** (`src/lib/oauth/program-scopes.ts`). Base `WIZARD_OAUTH_SCOPES` +**OAuth scopes** (`src/programs/oauth/program-scopes.ts`). Base `WIZARD_OAUTH_SCOPES` (`src/shared/constants.ts`) ∪ `SELF_DRIVING_SCOPE_ADDITIONS` — **12 strings**, requested via a PKCE auth-code flow: @@ -374,7 +374,7 @@ source is enabled. (`posthog/models/organization.py`, default `True`, nullable; admin toggle at `/settings/organization#organization-ai-consent`). Fail-closed; without it findings are silently dropped. Enforced for this program by the **base - wizard's AI opt-in gate** (`src/lib/programs/ai-opt-in-gate.ts`, + wizard's AI opt-in gate** (`src/programs/ai-opt-in-gate.ts`, `withAiOptInGate`): it injects an `ai-opt-in` step after `auth` for every program that doesn't set `requiresAi: false` (self-driving doesn't), and `store.getGate('ai-opt-in')` parks the agent until approval lands — so the @@ -583,11 +583,11 @@ must be running, or no scout ever dispatches. > (plus its own `startDelay` of 2 s); `ContentSequencer.handleComplete` > fires `onSequenceComplete` **only after the last block's `pause` > elapses**; the deck self-driving plays is the **shared factory default** > -> `agent-skill/content/index.tsx` (`getContentBlocks`, last block +> `src/ui/tui/decks/agent-skill/index.tsx` (`getContentBlocks`, last block > `pause: 60000`) — self-driving does **not** override it today. **Scoping > caveat (the whole reason this is a TODO, not a one-liner):** that deck is > inherited by _every_ skill program (audit, revenue-analytics, migration, -> bare `wizard skill `), so editing `agent-skill/content/index.tsx` +> bare `wizard skill `), so editing `src/ui/tui/decks/agent-skill/index.tsx` > changes all of them. Fix self-driving alone the way `getTips` already is — > add a **self-driving-owned `getContentBlocks`** override to > `selfDrivingConfig` (`self-driving/index.ts`, right next to the `getTips` @@ -958,7 +958,7 @@ Code anchors: posthog `products/replay_vision/backend/models/replay_scanner.py`, `api/scanners.py`, `temporal/scanners/prompts/signals_step.jinja` (the fixed defect-detection turn that `emits_signals` appends — the *why* the skill cares more about a scanner's `query` than its prompt); wizard `program-scopes.ts` + -`prompt.ts` + `content/tips.ts`; skill `6c-replay-vision-scanners.md`. +`prompt.ts` + `src/ui/tui/decks/self-driving/tips.ts`; skill `6c-replay-vision-scanners.md`. --- diff --git a/src/lib/programs/self-driving/__tests__/step-keys.test.ts b/src/programs/self-driving/__tests__/step-keys.test.ts similarity index 100% rename from src/lib/programs/self-driving/__tests__/step-keys.test.ts rename to src/programs/self-driving/__tests__/step-keys.test.ts diff --git a/src/lib/programs/self-driving/detect-agentic.ts b/src/programs/self-driving/detect-agentic.ts similarity index 94% rename from src/lib/programs/self-driving/detect-agentic.ts rename to src/programs/self-driving/detect-agentic.ts index 3822359de..2df306bf7 100644 --- a/src/lib/programs/self-driving/detect-agentic.ts +++ b/src/programs/self-driving/detect-agentic.ts @@ -3,7 +3,7 @@ * * Mirrors source-maps: the integration-detect screen runs `detectSelfDriving- * IntegrationProjects` (the shared integration scan from - * @lib/detection/project-scope), shows a project map, and the user picks one. + * @programs/detection/project-scope), shows a project map, and the user picks one. * `toIntegrationCandidates` matches each project to an `Integration`; this file * classifies it as instrumentable (a framework the wizard supports that doesn't * already have PostHog). The screen writes the choice to the session; `prepSelfDriving- @@ -14,12 +14,12 @@ import type { AgenticDetectionReport, DetectEvent, -} from '@lib/detection/agentic'; +} from '@programs/detection/agentic'; import { detectIntegrationProjects, toIntegrationCandidates, -} from '@lib/detection/project-scope'; -import { gatherFrameworkContext } from '@lib/detection/index'; +} from '@programs/detection/project-scope'; +import { gatherFrameworkContext } from '@programs/detection/index'; import type { Integration } from '@shared/constants'; import type { WizardSession } from '@lib/wizard-session'; diff --git a/src/lib/programs/self-driving/detect.ts b/src/programs/self-driving/detect.ts similarity index 98% rename from src/lib/programs/self-driving/detect.ts rename to src/programs/self-driving/detect.ts index a5dbdb546..e53598ed7 100644 --- a/src/lib/programs/self-driving/detect.ts +++ b/src/programs/self-driving/detect.ts @@ -31,8 +31,8 @@ import { analytics } from '@utils/analytics'; import type { WizardSession } from '@lib/wizard-session'; import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; -import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import { detectWarehouseSources } from '@programs/warehouse-sources/detect'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; /** frameworkContext key holding the deterministic PostHog-presence result. */ export const POSTHOG_PRESENT_KEY = 'postHogPresent'; @@ -65,7 +65,7 @@ export function getSelfDrivingDetectedTools( const POSTHOG_PACKAGE_RE = /(^|["'\s/=:.@])posthog/im; // Manifests grepped for a posthog dependency. Distinct from PROJECT_MANIFESTS -// in @lib/detection/agentic (project-root discovery); keep the two in sync +// in @programs/detection/agentic (project-root discovery); keep the two in sync // (a test asserts the shared ecosystem names appear in both). Exported for it. export const POSTHOG_MANIFESTS = [ 'package.json', diff --git a/src/lib/programs/self-driving/index.ts b/src/programs/self-driving/index.ts similarity index 94% rename from src/lib/programs/self-driving/index.ts rename to src/programs/self-driving/index.ts index 38a0ad930..b43e00fce 100644 --- a/src/lib/programs/self-driving/index.ts +++ b/src/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/programs/program-run'; +import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramRun } from '@programs/program-run'; import { OutroKind, type WizardSession } from '@lib/wizard-session'; import { createSkillProgram } from '../agent-skill/index.js'; import { SELF_DRIVING_PROGRAM } from './steps.js'; @@ -15,9 +15,9 @@ import { NO_DEFAULT_LIMIT, PRICE_PER_PR_USD, PRICING_LONG, -} from './content/pricing.js'; -import { getTips } from './content/tips.js'; -import { getContentBlocks } from './content/index.js'; +} from '../../ui/tui/decks/self-driving/pricing.js'; +import { getTips } from '../../ui/tui/decks/self-driving/tips.js'; +import { getContentBlocks } from '../../ui/tui/decks/self-driving/index.js'; export const SELF_DRIVING_SKILL_ID = 'self-driving-setup'; const REPORT_FILE = 'posthog-self-driving-report.md'; diff --git a/src/lib/programs/self-driving/prompt.ts b/src/programs/self-driving/prompt.ts similarity index 99% rename from src/lib/programs/self-driving/prompt.ts rename to src/programs/self-driving/prompt.ts index db629ea28..393586dc4 100644 --- a/src/lib/programs/self-driving/prompt.ts +++ b/src/programs/self-driving/prompt.ts @@ -1,6 +1,6 @@ import { AgentSignals } from '@agent'; import type { PromptContext } from '@agent/types'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; /** * Render the deterministic codebase-tool scan for the prompt. STEP 4 and diff --git a/src/lib/programs/self-driving/step-keys.ts b/src/programs/self-driving/step-keys.ts similarity index 100% rename from src/lib/programs/self-driving/step-keys.ts rename to src/programs/self-driving/step-keys.ts diff --git a/src/lib/programs/self-driving/steps.ts b/src/programs/self-driving/steps.ts similarity index 95% rename from src/lib/programs/self-driving/steps.ts rename to src/programs/self-driving/steps.ts index 4eeceb5d4..2bad6952c 100644 --- a/src/lib/programs/self-driving/steps.ts +++ b/src/programs/self-driving/steps.ts @@ -14,11 +14,11 @@ * gates on the GitHub App connection the run cannot proceed without. No keep-skills step: the setup skill is transient, so postRun removes it. */ -import type { ProgramStep } from '@lib/programs/program-step'; -import { resolveProjectDir } from '@lib/detection/agentic'; +import type { ProgramStep } from '@programs/program-step'; +import { resolveProjectDir } from '@programs/detection/agentic'; import { RunPhase, type WizardSession } from '@lib/wizard-session'; -import { HEALTH_CHECK_STEP } from '@lib/programs/shared/health-check-step'; -import { integrationRunStep } from '@lib/programs/posthog-integration/index'; +import { HEALTH_CHECK_STEP } from '@programs/shared/health-check-step'; +import { integrationRunStep } from '@programs/posthog-integration/index'; import { detectSelfDrivingPrerequisites, POSTHOG_PRESENT_KEY, diff --git a/src/lib/programs/self-driving/test/e2e.json b/src/programs/self-driving/test/e2e.json similarity index 100% rename from src/lib/programs/self-driving/test/e2e.json rename to src/programs/self-driving/test/e2e.json diff --git a/src/lib/programs/shared/health-check-step.ts b/src/programs/shared/health-check-step.ts similarity index 97% rename from src/lib/programs/shared/health-check-step.ts rename to src/programs/shared/health-check-step.ts index 8a42179eb..d7a147159 100644 --- a/src/lib/programs/shared/health-check-step.ts +++ b/src/programs/shared/health-check-step.ts @@ -11,7 +11,7 @@ * render the OutroScreen. */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import type { WizardSession } from '@lib/wizard-session'; import { evaluateWizardReadiness, diff --git a/src/lib/programs/shared/package-scanning.ts b/src/programs/shared/package-scanning.ts similarity index 100% rename from src/lib/programs/shared/package-scanning.ts rename to src/programs/shared/package-scanning.ts diff --git a/src/lib/programs/shared/posthog-cli-preinstall.ts b/src/programs/shared/posthog-cli-preinstall.ts similarity index 100% rename from src/lib/programs/shared/posthog-cli-preinstall.ts rename to src/programs/shared/posthog-cli-preinstall.ts diff --git a/src/lib/programs/slack/index.ts b/src/programs/slack/index.ts similarity index 91% rename from src/lib/programs/slack/index.ts rename to src/programs/slack/index.ts index 43823ef7d..e62a7f0b3 100644 --- a/src/lib/programs/slack/index.ts +++ b/src/programs/slack/index.ts @@ -6,7 +6,7 @@ * happens in the browser, so a wizard login adds nothing for the user. */ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; export const slackConnectConfig: ProgramConfig = { id: 'slack', diff --git a/src/lib/task-stream/__tests__/audit-areas.test.ts b/src/programs/task-stream/__tests__/audit-areas.test.ts similarity index 92% rename from src/lib/task-stream/__tests__/audit-areas.test.ts rename to src/programs/task-stream/__tests__/audit-areas.test.ts index 4996c1c19..02c989f9c 100644 --- a/src/lib/task-stream/__tests__/audit-areas.test.ts +++ b/src/programs/task-stream/__tests__/audit-areas.test.ts @@ -1,9 +1,9 @@ import { rollUpAuditAreas, MAX_AUDIT_AREAS, -} from '@lib/task-stream/audit-areas'; -import { StreamTaskStatus } from '@lib/task-stream/types'; -import type { AuditCheck } from '@lib/programs/audit/types'; +} from '@programs/task-stream/audit-areas'; +import { StreamTaskStatus } from '@programs/task-stream/types'; +import type { AuditCheck } from '@programs/audit/types'; const check = (over: Partial): AuditCheck => ({ id: 'id', diff --git a/src/lib/task-stream/__tests__/event-plan-watcher.test.ts b/src/programs/task-stream/__tests__/event-plan-watcher.test.ts similarity index 97% rename from src/lib/task-stream/__tests__/event-plan-watcher.test.ts rename to src/programs/task-stream/__tests__/event-plan-watcher.test.ts index 81bde5be2..af68b9838 100644 --- a/src/lib/task-stream/__tests__/event-plan-watcher.test.ts +++ b/src/programs/task-stream/__tests__/event-plan-watcher.test.ts @@ -10,8 +10,8 @@ import { join } from 'node:path'; import { EventPlanWatcher, normalizeEventPlan, -} from '@lib/task-stream/event-plan-watcher'; -import { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/constants'; +} from '@programs/task-stream/event-plan-watcher'; +import { EVENT_PLAN_FILE } from '@programs/posthog-integration/constants'; import type { PlannedEvent, WizardStore } from '@ui/tui/store'; const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/lib/task-stream/__tests__/file-destination.test.ts b/src/programs/task-stream/__tests__/file-destination.test.ts similarity index 94% rename from src/lib/task-stream/__tests__/file-destination.test.ts rename to src/programs/task-stream/__tests__/file-destination.test.ts index ecb97622d..f86700e8d 100644 --- a/src/lib/task-stream/__tests__/file-destination.test.ts +++ b/src/programs/task-stream/__tests__/file-destination.test.ts @@ -4,8 +4,11 @@ import { join } from 'node:path'; import { FileDestination, createFileDestination, -} from '@lib/task-stream/destinations/file'; -import { StreamEvent, type TaskStreamUpdate } from '@lib/task-stream/types'; +} from '@programs/task-stream/destinations/file'; +import { + StreamEvent, + type TaskStreamUpdate, +} from '@programs/task-stream/types'; import { WIZARD_TASK_STREAM_FILE } from '@utils/paths'; import { RunPhase } from '@lib/wizard-session'; diff --git a/src/lib/task-stream/__tests__/posthog-destination.test.ts b/src/programs/task-stream/__tests__/posthog-destination.test.ts similarity index 99% rename from src/lib/task-stream/__tests__/posthog-destination.test.ts rename to src/programs/task-stream/__tests__/posthog-destination.test.ts index 48035d7f6..d7325ab50 100644 --- a/src/lib/task-stream/__tests__/posthog-destination.test.ts +++ b/src/programs/task-stream/__tests__/posthog-destination.test.ts @@ -1,6 +1,6 @@ import { PostHogDestination } from '../destinations/posthog'; import { StreamEvent, type TaskStreamUpdate } from '../types'; -import { RunPhase, type Credentials } from '../../wizard-session'; +import { RunPhase, type Credentials } from '../../../lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; const SAMPLE_CREDS: Credentials = { diff --git a/src/lib/task-stream/__tests__/task-stream-push.test.ts b/src/programs/task-stream/__tests__/task-stream-push.test.ts similarity index 98% rename from src/lib/task-stream/__tests__/task-stream-push.test.ts rename to src/programs/task-stream/__tests__/task-stream-push.test.ts index 52272ffd5..cfae3e18b 100644 --- a/src/lib/task-stream/__tests__/task-stream-push.test.ts +++ b/src/programs/task-stream/__tests__/task-stream-push.test.ts @@ -1,16 +1,16 @@ -import { TaskStreamPush } from '@lib/task-stream/task-stream-push'; -import { StreamEvent, StreamTaskStatus } from '@lib/task-stream/types'; +import { TaskStreamPush } from '@programs/task-stream/task-stream-push'; +import { StreamEvent, StreamTaskStatus } from '@programs/task-stream/types'; import type { TaskStreamDestination, TaskStreamUpdate, -} from '@lib/task-stream/types'; +} from '@programs/task-stream/types'; import type { WizardStore, TaskItem } from '@ui/tui/store'; import { TaskStatus } from '@ui/wizard-ui'; import { RunPhase, type PendingQuestion } from '@lib/wizard-session'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/constants'; +import { EVENT_PLAN_FILE } from '@programs/posthog-integration/constants'; type Listener = () => void; diff --git a/src/lib/task-stream/audit-areas.ts b/src/programs/task-stream/audit-areas.ts similarity index 96% rename from src/lib/task-stream/audit-areas.ts rename to src/programs/task-stream/audit-areas.ts index db77679cb..53b24440b 100644 --- a/src/lib/task-stream/audit-areas.ts +++ b/src/programs/task-stream/audit-areas.ts @@ -6,7 +6,7 @@ * A finding is progress, not a failure, so an area never reports `failed`. */ -import type { AuditCheck, AuditStatus } from '@lib/programs/audit/types'; +import type { AuditCheck, AuditStatus } from '@programs/audit/types'; import { StreamTaskStatus, type StreamTask } from './types'; export const MAX_AUDIT_AREAS = 24; diff --git a/src/lib/task-stream/destinations/file.ts b/src/programs/task-stream/destinations/file.ts similarity index 98% rename from src/lib/task-stream/destinations/file.ts rename to src/programs/task-stream/destinations/file.ts index 966024ce6..c082c6d36 100644 --- a/src/lib/task-stream/destinations/file.ts +++ b/src/programs/task-stream/destinations/file.ts @@ -16,7 +16,7 @@ import type { StreamEvent, TaskStreamDestination, TaskStreamUpdate, -} from '@lib/task-stream/types'; +} from '@programs/task-stream/types'; export interface FileDestinationOptions { path: string; diff --git a/src/lib/task-stream/destinations/posthog.ts b/src/programs/task-stream/destinations/posthog.ts similarity index 99% rename from src/lib/task-stream/destinations/posthog.ts rename to src/programs/task-stream/destinations/posthog.ts index b25c53e70..061a0d8a8 100644 --- a/src/lib/task-stream/destinations/posthog.ts +++ b/src/programs/task-stream/destinations/posthog.ts @@ -21,7 +21,7 @@ import type { TaskStreamDestination, TaskStreamUpdate, StreamEvent, -} from '@lib/task-stream/types'; +} from '@programs/task-stream/types'; import type { Credentials } from '@lib/wizard-session'; import { logToFile } from '@utils/debug'; diff --git a/src/lib/task-stream/event-plan-watcher.ts b/src/programs/task-stream/event-plan-watcher.ts similarity index 100% rename from src/lib/task-stream/event-plan-watcher.ts rename to src/programs/task-stream/event-plan-watcher.ts diff --git a/src/lib/task-stream/index.ts b/src/programs/task-stream/index.ts similarity index 100% rename from src/lib/task-stream/index.ts rename to src/programs/task-stream/index.ts diff --git a/src/lib/task-stream/task-stream-push.ts b/src/programs/task-stream/task-stream-push.ts similarity index 100% rename from src/lib/task-stream/task-stream-push.ts rename to src/programs/task-stream/task-stream-push.ts diff --git a/src/lib/task-stream/types.ts b/src/programs/task-stream/types.ts similarity index 100% rename from src/lib/task-stream/types.ts rename to src/programs/task-stream/types.ts diff --git a/src/programs/types.ts b/src/programs/types.ts new file mode 100644 index 000000000..620e94d18 --- /dev/null +++ b/src/programs/types.ts @@ -0,0 +1,9 @@ +/** Public type entry for the programs surface. */ +export type { ProgramId, SubcommandProgram } from './program-registry'; +export type { + ProgramConfig, + ProgramStep, + ProgramReadyContext, + StoreInitContext, +} from './program-step'; +export type { FrameworkConfig, SetupQuestion } from './framework-config'; diff --git a/src/lib/programs/warehouse-source/detect.ts b/src/programs/warehouse-source/detect.ts similarity index 96% rename from src/lib/programs/warehouse-source/detect.ts rename to src/programs/warehouse-source/detect.ts index 22a7b79ab..0fa74c37e 100644 --- a/src/lib/programs/warehouse-source/detect.ts +++ b/src/programs/warehouse-source/detect.ts @@ -10,8 +10,8 @@ import { existsSync, statSync } from 'fs'; import { analytics } from '@utils/analytics'; import type { WizardSession } from '@lib/wizard-session'; import type { AbortCase } from '@agent/types'; -import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; -import type { DetectedSource } from '@lib/warehouse-sources/types'; +import { detectWarehouseSources } from '@programs/warehouse-sources/detect'; +import type { DetectedSource } from '@programs/warehouse-sources/types'; /** Structured detection errors rendered by the intro screen. */ export type WarehouseDetectError = diff --git a/src/lib/programs/warehouse-source/index.ts b/src/programs/warehouse-source/index.ts similarity index 93% rename from src/lib/programs/warehouse-source/index.ts rename to src/programs/warehouse-source/index.ts index a2fb7db06..fb23dd28c 100644 --- a/src/lib/programs/warehouse-source/index.ts +++ b/src/programs/warehouse-source/index.ts @@ -1,5 +1,5 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; -import type { ProgramRun } from '@lib/programs/program-run'; +import type { ProgramConfig } from '@programs/program-step'; +import type { ProgramRun } from '@programs/program-run'; import type { WizardSession } from '@lib/wizard-session'; import { LONGER_ASK_TIMEOUT_MS } from '@agent'; import { WAREHOUSE_SOURCE_PROGRAM } from './steps.js'; @@ -7,7 +7,7 @@ import { WAREHOUSE_ABORT_CASES, getDetectedWarehouseSources, } from './detect.js'; -import { getContentBlocks } from './content/index.js'; +import { getContentBlocks } from '../../ui/tui/decks/warehouse-source/index.js'; /** * Inject the detected sources (and their creation mode) into the prompt so the diff --git a/src/lib/programs/warehouse-source/steps.ts b/src/programs/warehouse-source/steps.ts similarity index 95% rename from src/lib/programs/warehouse-source/steps.ts rename to src/programs/warehouse-source/steps.ts index 1a64bfebd..0a8ed69db 100644 --- a/src/lib/programs/warehouse-source/steps.ts +++ b/src/programs/warehouse-source/steps.ts @@ -6,7 +6,7 @@ * both in-CLI source creation and deep-link emission per detected source. */ -import type { ProgramStep } from '@lib/programs/program-step'; +import type { ProgramStep } from '@programs/program-step'; import { RunPhase } from '@lib/wizard-session'; import { detectWarehousePrerequisites } from './detect.js'; diff --git a/src/lib/programs/warehouse-source/test/e2e.json b/src/programs/warehouse-source/test/e2e.json similarity index 100% rename from src/lib/programs/warehouse-source/test/e2e.json rename to src/programs/warehouse-source/test/e2e.json diff --git a/src/lib/warehouse-sources/__tests__/detect.test.ts b/src/programs/warehouse-sources/__tests__/detect.test.ts similarity index 99% rename from src/lib/warehouse-sources/__tests__/detect.test.ts rename to src/programs/warehouse-sources/__tests__/detect.test.ts index e6e773065..195e2511c 100644 --- a/src/lib/warehouse-sources/__tests__/detect.test.ts +++ b/src/programs/warehouse-sources/__tests__/detect.test.ts @@ -5,9 +5,9 @@ import { detectWarehouseSources, parseGemfile, parseEnvKeys, -} from '@lib/warehouse-sources/detect'; +} from '@programs/warehouse-sources/detect'; import { MAX_REPORTED_PATH_LENGTH } from '@utils/env-scan'; -import { SOURCE_DETECTORS } from '@lib/warehouse-sources/registry'; +import { SOURCE_DETECTORS } from '@programs/warehouse-sources/registry'; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'warehouse-detect-')); diff --git a/src/lib/warehouse-sources/detect.ts b/src/programs/warehouse-sources/detect.ts similarity index 99% rename from src/lib/warehouse-sources/detect.ts rename to src/programs/warehouse-sources/detect.ts index 2153a9a69..2f731bb4e 100644 --- a/src/lib/warehouse-sources/detect.ts +++ b/src/programs/warehouse-sources/detect.ts @@ -23,7 +23,7 @@ import { parseRequirementsTxt, parsePyprojectToml, parsePipfile, -} from '@lib/detection/features'; +} from '@programs/detection/features'; import { SOURCE_DETECTORS } from './registry.js'; import type { DetectedSource, SourceDetector } from './types.js'; diff --git a/src/lib/warehouse-sources/registry.ts b/src/programs/warehouse-sources/registry.ts similarity index 100% rename from src/lib/warehouse-sources/registry.ts rename to src/programs/warehouse-sources/registry.ts diff --git a/src/lib/warehouse-sources/types.ts b/src/programs/warehouse-sources/types.ts similarity index 95% rename from src/lib/warehouse-sources/types.ts rename to src/programs/warehouse-sources/types.ts index dbd62c56e..81dc78c44 100644 --- a/src/lib/warehouse-sources/types.ts +++ b/src/programs/warehouse-sources/types.ts @@ -24,7 +24,7 @@ export interface SourceSignals { npm?: string[]; /** * Python dependency names as normalized by the parsers in - * `@lib/detection/features` (lowercased, `_`→`-`), e.g. ['psycopg', 'psycopg2']. + * `@programs/detection/features` (lowercased, `_`→`-`), e.g. ['psycopg', 'psycopg2']. */ python?: string[]; /** Gemfile gem names, e.g. ['pg']. */ diff --git a/src/lib/programs/web-analytics-doctor/detect.ts b/src/programs/web-analytics-doctor/detect.ts similarity index 97% rename from src/lib/programs/web-analytics-doctor/detect.ts rename to src/programs/web-analytics-doctor/detect.ts index 9a05bbc00..e7df63673 100644 --- a/src/lib/programs/web-analytics-doctor/detect.ts +++ b/src/programs/web-analytics-doctor/detect.ts @@ -2,7 +2,7 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; import type { AbortCase } from '@agent/types'; import { ErrorCodes } from '@shared/errors'; -import { findPackageJsons } from '@lib/programs/shared/package-scanning'; +import { findPackageJsons } from '@programs/shared/package-scanning'; export type WebAnalyticsDetectError = | { diff --git a/src/lib/programs/web-analytics-doctor/index.ts b/src/programs/web-analytics-doctor/index.ts similarity index 96% rename from src/lib/programs/web-analytics-doctor/index.ts rename to src/programs/web-analytics-doctor/index.ts index ee360dd0e..4d11413c2 100644 --- a/src/lib/programs/web-analytics-doctor/index.ts +++ b/src/programs/web-analytics-doctor/index.ts @@ -1,4 +1,4 @@ -import type { ProgramConfig } from '@lib/programs/program-step'; +import type { ProgramConfig } from '@programs/program-step'; import { createSkillProgram } from '../agent-skill/index.js'; import { WEB_ANALYTICS_DOCTOR_PROGRAM } from './steps.js'; import { WEB_ANALYTICS_ABORT_CASES } from './detect.js'; diff --git a/src/lib/programs/web-analytics-doctor/steps.ts b/src/programs/web-analytics-doctor/steps.ts similarity index 70% rename from src/lib/programs/web-analytics-doctor/steps.ts rename to src/programs/web-analytics-doctor/steps.ts index 941b58a9d..a213d7b31 100644 --- a/src/lib/programs/web-analytics-doctor/steps.ts +++ b/src/programs/web-analytics-doctor/steps.ts @@ -1,5 +1,5 @@ -import type { ProgramStep } from '@lib/programs/program-step'; -import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/steps'; +import type { ProgramStep } from '@programs/program-step'; +import { AGENT_SKILL_STEPS } from '@programs/agent-skill/steps'; import { detectWebAnalyticsPrerequisites } from './detect.js'; export const WEB_ANALYTICS_DOCTOR_PROGRAM: ProgramStep[] = [ diff --git a/src/agent/__tests__/agent-env-isolation.test.ts b/src/shared/__tests__/agent-env-isolation.test.ts similarity index 99% rename from src/agent/__tests__/agent-env-isolation.test.ts rename to src/shared/__tests__/agent-env-isolation.test.ts index 3a8c9c6d6..196a05142 100644 --- a/src/agent/__tests__/agent-env-isolation.test.ts +++ b/src/shared/__tests__/agent-env-isolation.test.ts @@ -2,7 +2,7 @@ import { sanitizeAgentSubprocessEnv, isBlockedAgentEnvKey, BLOCKED_AGENT_ENV_KEYS, -} from '@agent/agent-env-isolation'; +} from '@shared/agent-env-isolation'; describe('isBlockedAgentEnvKey', () => { it('blocks the direct API key that outranks the gateway auth token', () => { diff --git a/src/agent/agent-env-isolation.ts b/src/shared/agent-env-isolation.ts similarity index 100% rename from src/agent/agent-env-isolation.ts rename to src/shared/agent-env-isolation.ts diff --git a/src/shared/claude-settings.ts b/src/shared/claude-settings.ts index 05d697273..43da0b418 100644 --- a/src/shared/claude-settings.ts +++ b/src/shared/claude-settings.ts @@ -12,13 +12,10 @@ import * as fs from 'fs'; import * as os from 'os'; import { analytics } from '@utils/analytics'; import { registerCleanup } from '@utils/wizard-abort'; -// Leaf module on purpose: shared analytics imports the session, which this -// file feeds, so the agent entry would form a module cycle here. -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B1: shared stops feeding the session import { BLOCKED_AGENT_ENV_KEYS, BLOCKED_AGENT_ENV_PATTERNS, -} from '@agent/agent-env-isolation'; +} from './agent-env-isolation'; /** The file names Claude Code accepts for project settings, in priority order. */ const SETTINGS_FILENAMES = ['settings.json', 'settings'] as const; diff --git a/src/shared/errors/index.ts b/src/shared/errors/index.ts index 67178ff72..c617753c7 100644 --- a/src/shared/errors/index.ts +++ b/src/shared/errors/index.ts @@ -8,7 +8,6 @@ export { ERROR_CATALOG } from './catalog'; export { WizardError } from './wizard-error'; export type { ErrorCatalogEntry, ErrorGroup, RetryAdvice } from './types'; export { classifyAuthFailure, type AuthFailureInput } from './auth'; -export { AGENT_ERROR_CODE } from './agent-map'; export { skillErrorCode } from './skill-map'; export { PHW_ERROR_PREFIX, diff --git a/src/shared/utils/__tests__/oauth.test.ts b/src/shared/utils/__tests__/oauth.test.ts index 6466d36b4..a77b35c49 100644 --- a/src/shared/utils/__tests__/oauth.test.ts +++ b/src/shared/utils/__tests__/oauth.test.ts @@ -10,7 +10,7 @@ import { WIZARD_OAUTH_SCOPES, WIZARD_PROVISIONING_SCOPES, } from '@shared/constants'; -import { getOAuthScopesForProgram } from '@lib/oauth/program-scopes'; +import { getOAuthScopesForProgram } from '@programs/oauth/program-scopes'; describe('extractOAuthCode', () => { it('extracts the code from a full callback URL', () => { diff --git a/src/shared/utils/env-scan.ts b/src/shared/utils/env-scan.ts index 866b9826b..0a6741514 100644 --- a/src/shared/utils/env-scan.ts +++ b/src/shared/utils/env-scan.ts @@ -3,7 +3,7 @@ * "which env keys does this project define, and in which file". * * Two surfaces read env keys and they must agree: - * - the warehouse-source detector (`@lib/warehouse-sources/detect`), which + * - the warehouse-source detector (`@programs/warehouse-sources/detect`), which * turns key names into detected sources, and * - the `check_env_keys` wizard tool, which answers the agent's * "is this key already set?" question. diff --git a/src/shared/utils/setup-utils.ts b/src/shared/utils/setup-utils.ts index f8857b555..0b327017d 100644 --- a/src/shared/utils/setup-utils.ts +++ b/src/shared/utils/setup-utils.ts @@ -18,8 +18,8 @@ import { DUMMY_PROJECT_API_KEY, ISSUES_URL } from '@shared/constants'; import { getOAuthScopesForProgram, getProvisioningScopesForProgram, -} from '@lib/oauth/program-scopes'; -import type { ProgramId } from '@lib/programs/program-registry'; +} from '@programs/oauth/program-scopes'; +import type { ProgramId } from '@programs/types'; import { analytics } from './analytics'; import { getUI } from '@ui'; import { HostResolution } from '@shared/host-resolution'; diff --git a/src/ui/tui/__tests__/frames.test.tsx b/src/ui/tui/__tests__/frames.test.tsx index 49bfb6062..935331e94 100644 --- a/src/ui/tui/__tests__/frames.test.tsx +++ b/src/ui/tui/__tests__/frames.test.tsx @@ -62,22 +62,22 @@ vi.mock('@ui/tui/hooks/useGithubConnection', () => ({ useGithubConnection: () => undefined, fetchLoginUrl: vi.fn().mockResolvedValue(null), })); -vi.mock('@lib/programs/self-driving/detect-agentic', async (actual) => ({ +vi.mock('@programs/self-driving/detect-agentic', async (actual) => ({ ...(await actual>()), detectSelfDrivingIntegrationProjects: vi.fn(pending), })); -vi.mock('@lib/programs/error-tracking/detect-agentic', async (actual) => ({ +vi.mock('@programs/error-tracking/detect-agentic', async (actual) => ({ ...(await actual>()), detectErrorTrackingProjects: vi.fn(pending), })); vi.mock( - '@lib/programs/error-tracking-upload-source-maps/detect-agentic', + '@programs/error-tracking-upload-source-maps/detect-agentic', async (actual) => ({ ...(await actual>()), detectSourceMapsProjects: vi.fn(pending), }), ); -vi.mock('@lib/programs/posthog-doctor/fetch', () => ({ +vi.mock('@programs/posthog-doctor/fetch', () => ({ fetchHealthIssues: vi.fn().mockResolvedValue([ { id: 'issue-1', @@ -113,17 +113,17 @@ import { } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import type { FrameworkConfig } from '@lib/framework-config'; -import { Program, type ProgramId } from '@lib/programs/program-registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import type { FrameworkConfig } from '@programs/types'; +import { Program, type ProgramId } from '@programs'; import { WizardReadiness, type WizardReadinessResult, } from '@shared/health-checks/readiness'; import { ServiceHealthStatus } from '@shared/health-checks/types'; -import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/detect'; -import { AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; -import { AUDIT_SEED_CHECKS } from '@lib/programs/audit/seed'; +import { SOURCE_MAPS_CONTEXT_KEYS } from '@programs/error-tracking-upload-source-maps/detect'; +import { AUDIT_CHECKS_KEY } from '@programs/audit/types'; +import { AUDIT_SEED_CHECKS } from '@programs/audit/seed'; import type { McpInstaller } from '@ui/tui/services/mcp-installer'; import type { McpSuggestedPromptsServices } from '@ui/tui/services/mcp-suggested-prompts-services'; import { diff --git a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx index 1a5c27db2..43e9d24cb 100644 --- a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx +++ b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx @@ -25,11 +25,11 @@ import { type WizardSession, } from '@lib/wizard-session'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { HostResolution } from '@shared/host-resolution'; import { WizardReadiness } from '@shared/health-checks/readiness'; -import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/detect'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { SOURCE_MAPS_CONTEXT_KEYS } from '@programs/error-tracking-upload-source-maps/detect'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; import { ScreenContainer } from '../primitives/ScreenContainer'; import { createScreens, diff --git a/src/ui/tui/__tests__/programs.test.ts b/src/ui/tui/__tests__/programs.test.ts index 7df542c4a..f09e9fa14 100644 --- a/src/ui/tui/__tests__/programs.test.ts +++ b/src/ui/tui/__tests__/programs.test.ts @@ -1,7 +1,7 @@ import { buildSession, McpOutcome, RunPhase } from '@lib/wizard-session'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { PROGRAM_SEQUENCES, ScreenId } from '@ui/tui/screen-sequences'; -import { Program, type ProgramId } from '@lib/programs/program-registry'; +import { Program, type ProgramId } from '@programs'; function getEntry(program: ProgramId, id: ScreenId) { const entry = PROGRAM_SEQUENCES[program].find( diff --git a/src/ui/tui/__tests__/router.test.ts b/src/ui/tui/__tests__/router.test.ts index 89b94150f..2f20c0608 100644 --- a/src/ui/tui/__tests__/router.test.ts +++ b/src/ui/tui/__tests__/router.test.ts @@ -8,8 +8,8 @@ import { HostResolution } from '@shared/host-resolution'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { WizardRouter, ScreenId, Overlay, Program } from '@ui/tui/router'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { PROGRAM_REGISTRY } from '@programs'; function baseWizardSession() { return buildSession({}); diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts index a8939578b..2c42514bf 100644 --- a/src/ui/tui/__tests__/store-invariants.test.ts +++ b/src/ui/tui/__tests__/store-invariants.test.ts @@ -29,9 +29,9 @@ import { PROGRAM_SEQUENCES } from '@ui/tui/screen-sequences'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { analytics } from '@utils/analytics'; -import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; +import { PROGRAM_REGISTRY } from '@programs'; import type { SettingsConflict } from '@shared/claude-settings'; vi.mock('@utils/analytics.js', () => ({ diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts index ed8cddf1c..dce0ea1e4 100644 --- a/src/ui/tui/__tests__/store.test.ts +++ b/src/ui/tui/__tests__/store.test.ts @@ -18,7 +18,7 @@ import { buildSession } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; import { analytics } from '@utils/analytics'; -import { getProgramConfig } from '@lib/programs/program-registry'; +import { getProgramConfig } from '@programs'; vi.mock('@utils/analytics.js', () => ({ analytics: { diff --git a/src/ui/tui/components/LearnCard.tsx b/src/ui/tui/components/LearnCard.tsx index be6dfc65f..8a5b2f265 100644 --- a/src/ui/tui/components/LearnCard.tsx +++ b/src/ui/tui/components/LearnCard.tsx @@ -1,8 +1,8 @@ /** * LearnCard — Generic render shell for an animated content deck. * - * Program-owned. Callers pass the script via `blocks`. The script lives - * under `src/lib/programs//content/`. The shell handles + * Callers pass the script via `blocks`. The script lives under + * `src/ui/tui/decks//`. The shell handles * dimension tracking, status-bar height math, and the `display="none"` * clamp on narrow terminals. */ diff --git a/src/lib/programs/agent-skill/content/index.tsx b/src/ui/tui/decks/agent-skill/index.tsx similarity index 100% rename from src/lib/programs/agent-skill/content/index.tsx rename to src/ui/tui/decks/agent-skill/index.tsx diff --git a/src/lib/programs/error-tracking-upload-source-maps/content/index.tsx b/src/ui/tui/decks/error-tracking-upload-source-maps/index.tsx similarity index 100% rename from src/lib/programs/error-tracking-upload-source-maps/content/index.tsx rename to src/ui/tui/decks/error-tracking-upload-source-maps/index.tsx diff --git a/src/lib/programs/error-tracking/content/index.tsx b/src/ui/tui/decks/error-tracking/index.tsx similarity index 85% rename from src/lib/programs/error-tracking/content/index.tsx rename to src/ui/tui/decks/error-tracking/index.tsx index 815ad34fc..39ebbd83d 100644 --- a/src/lib/programs/error-tracking/content/index.tsx +++ b/src/ui/tui/decks/error-tracking/index.tsx @@ -2,7 +2,7 @@ import type { WizardStore } from '@ui/tui/store'; import type { ContentBlock } from '@ui/tui/primitives/content-types'; -import { buildSourceMapsDeck } from '@lib/programs/error-tracking-upload-source-maps/content/index'; +import { buildSourceMapsDeck } from '@ui/tui/decks/error-tracking-upload-source-maps/index'; export const getContentBlocks = (store?: WizardStore): ContentBlock[] => buildSourceMapsDeck(store, { diff --git a/src/lib/programs/error-tracking/content/tips.ts b/src/ui/tui/decks/error-tracking/tips.ts similarity index 93% rename from src/lib/programs/error-tracking/content/tips.ts rename to src/ui/tui/decks/error-tracking/tips.ts index edf968de4..dba3d8e68 100644 --- a/src/lib/programs/error-tracking/content/tips.ts +++ b/src/ui/tui/decks/error-tracking/tips.ts @@ -1,7 +1,7 @@ /** Sidebar tips for the error-tracking run: product features the learn deck does not cover. */ import type { Tip } from '@ui/tui/components/TipsCard'; -import { REPLAY_VISION_SUPPORTED } from '@lib/programs/replay-vision/index'; +import { REPLAY_VISION_SUPPORTED } from '@programs/replay-vision/index'; export const ERROR_TRACKING_TIPS: Tip[] = [ { diff --git a/src/lib/programs/migration/content/free-tier.tsx b/src/ui/tui/decks/migration/free-tier.tsx similarity index 100% rename from src/lib/programs/migration/content/free-tier.tsx rename to src/ui/tui/decks/migration/free-tier.tsx diff --git a/src/lib/programs/migration/content/index.tsx b/src/ui/tui/decks/migration/index.tsx similarity index 96% rename from src/lib/programs/migration/content/index.tsx rename to src/ui/tui/decks/migration/index.tsx index 152e80e8c..3eaff5927 100644 --- a/src/lib/programs/migration/content/index.tsx +++ b/src/ui/tui/decks/migration/index.tsx @@ -21,9 +21,9 @@ import { Colors } from '@ui/tui/styles'; import { TextRevealMode } from '@ui/tui/primitives/TextBlock'; import type { ContentBlock } from '@ui/tui/primitives/content-types'; import { StatusPeekTrigger } from '@ui/tui/components/StatusPeekTrigger'; -import { PRODUCT_SUITE_BLOCK } from '@lib/programs/posthog-integration/content/product-suite'; -import { LINE_CHART_BLOCK } from '@lib/programs/posthog-integration/content/line-chart'; -import { FUNNEL_BLOCK } from '@lib/programs/posthog-integration/content/funnel'; +import { PRODUCT_SUITE_BLOCK } from '@ui/tui/decks/posthog-integration/product-suite'; +import { LINE_CHART_BLOCK } from '@ui/tui/decks/posthog-integration/line-chart'; +import { FUNNEL_BLOCK } from '@ui/tui/decks/posthog-integration/funnel'; import { VENDOR_STACK_BLOCK } from './vendor-stack.js'; import { FREE_TIER_BLOCK } from './free-tier.js'; import { PRICING_STRUCTURE_BLOCK } from './pricing-structure.js'; diff --git a/src/lib/programs/migration/content/pricing-structure.tsx b/src/ui/tui/decks/migration/pricing-structure.tsx similarity index 100% rename from src/lib/programs/migration/content/pricing-structure.tsx rename to src/ui/tui/decks/migration/pricing-structure.tsx diff --git a/src/lib/programs/migration/content/vendor-stack.tsx b/src/ui/tui/decks/migration/vendor-stack.tsx similarity index 100% rename from src/lib/programs/migration/content/vendor-stack.tsx rename to src/ui/tui/decks/migration/vendor-stack.tsx diff --git a/src/lib/programs/posthog-integration/content/data-flow.tsx b/src/ui/tui/decks/posthog-integration/data-flow.tsx similarity index 100% rename from src/lib/programs/posthog-integration/content/data-flow.tsx rename to src/ui/tui/decks/posthog-integration/data-flow.tsx diff --git a/src/lib/programs/posthog-integration/content/funnel.tsx b/src/ui/tui/decks/posthog-integration/funnel.tsx similarity index 100% rename from src/lib/programs/posthog-integration/content/funnel.tsx rename to src/ui/tui/decks/posthog-integration/funnel.tsx diff --git a/src/lib/programs/posthog-integration/content/index.tsx b/src/ui/tui/decks/posthog-integration/index.tsx similarity index 100% rename from src/lib/programs/posthog-integration/content/index.tsx rename to src/ui/tui/decks/posthog-integration/index.tsx diff --git a/src/lib/programs/posthog-integration/content/line-chart.tsx b/src/ui/tui/decks/posthog-integration/line-chart.tsx similarity index 100% rename from src/lib/programs/posthog-integration/content/line-chart.tsx rename to src/ui/tui/decks/posthog-integration/line-chart.tsx diff --git a/src/lib/programs/posthog-integration/content/product-suite.tsx b/src/ui/tui/decks/posthog-integration/product-suite.tsx similarity index 100% rename from src/lib/programs/posthog-integration/content/product-suite.tsx rename to src/ui/tui/decks/posthog-integration/product-suite.tsx diff --git a/src/lib/programs/revenue-analytics/content/index.tsx b/src/ui/tui/decks/revenue-analytics/index.tsx similarity index 74% rename from src/lib/programs/revenue-analytics/content/index.tsx rename to src/ui/tui/decks/revenue-analytics/index.tsx index 1abb4ce84..974ebc431 100644 --- a/src/lib/programs/revenue-analytics/content/index.tsx +++ b/src/ui/tui/decks/revenue-analytics/index.tsx @@ -4,4 +4,4 @@ * revenue narrative grows its own diagrams or talking points. */ -export { getContentBlocks } from '@lib/programs/agent-skill/content/index'; +export { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; diff --git a/src/lib/programs/self-driving/content/index.tsx b/src/ui/tui/decks/self-driving/index.tsx similarity index 100% rename from src/lib/programs/self-driving/content/index.tsx rename to src/ui/tui/decks/self-driving/index.tsx diff --git a/src/lib/programs/self-driving/content/pipeline-diagram.tsx b/src/ui/tui/decks/self-driving/pipeline-diagram.tsx similarity index 100% rename from src/lib/programs/self-driving/content/pipeline-diagram.tsx rename to src/ui/tui/decks/self-driving/pipeline-diagram.tsx diff --git a/src/lib/programs/self-driving/content/pricing.ts b/src/ui/tui/decks/self-driving/pricing.ts similarity index 100% rename from src/lib/programs/self-driving/content/pricing.ts rename to src/ui/tui/decks/self-driving/pricing.ts diff --git a/src/lib/programs/self-driving/content/tips.ts b/src/ui/tui/decks/self-driving/tips.ts similarity index 100% rename from src/lib/programs/self-driving/content/tips.ts rename to src/ui/tui/decks/self-driving/tips.ts diff --git a/src/lib/programs/warehouse-source/content/index.tsx b/src/ui/tui/decks/warehouse-source/index.tsx similarity index 73% rename from src/lib/programs/warehouse-source/content/index.tsx rename to src/ui/tui/decks/warehouse-source/index.tsx index 4be0eab20..0f364bf00 100644 --- a/src/lib/programs/warehouse-source/content/index.tsx +++ b/src/ui/tui/decks/warehouse-source/index.tsx @@ -4,4 +4,4 @@ * narrative grows its own diagrams or talking points. */ -export { getContentBlocks } from '@lib/programs/agent-skill/content/index'; +export { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; diff --git a/src/ui/tui/playground/demos/AuditChecksDemo.tsx b/src/ui/tui/playground/demos/AuditChecksDemo.tsx index 720498694..fab8e01fb 100644 --- a/src/ui/tui/playground/demos/AuditChecksDemo.tsx +++ b/src/ui/tui/playground/demos/AuditChecksDemo.tsx @@ -5,7 +5,7 @@ */ import { Box } from 'ink'; -import type { AuditCheck } from '@lib/programs/audit/types'; +import type { AuditCheck } from '@programs/audit/types'; import { AuditChecksViewer } from '@ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer'; const MOCK_CHECKS: AuditCheck[] = [ diff --git a/src/ui/tui/playground/demos/DoctorReportDemo.tsx b/src/ui/tui/playground/demos/DoctorReportDemo.tsx index 3dcf61a77..9a7adc6a0 100644 --- a/src/ui/tui/playground/demos/DoctorReportDemo.tsx +++ b/src/ui/tui/playground/demos/DoctorReportDemo.tsx @@ -1,7 +1,7 @@ import { Box, Text } from 'ink'; import { Colors, Icons } from '@ui/tui/styles'; import { IssueTable } from '@ui/tui/screens/doctor/IssueTable'; -import type { HealthIssue } from '@lib/programs/posthog-doctor/index'; +import type { HealthIssue } from '@programs/posthog-doctor/index'; const NOW = '2026-04-27T15:00:00Z'; diff --git a/src/ui/tui/playground/demos/LearnDeckDemo.tsx b/src/ui/tui/playground/demos/LearnDeckDemo.tsx index a38e3ecf3..b94b38796 100644 --- a/src/ui/tui/playground/demos/LearnDeckDemo.tsx +++ b/src/ui/tui/playground/demos/LearnDeckDemo.tsx @@ -28,7 +28,7 @@ import { import type { ContentBlock, ProgressItem } from '@ui/tui/primitives/index'; import { Colors } from '@ui/tui/styles'; import type { WizardStore } from '@ui/tui/store'; -import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; +import { PROGRAM_REGISTRY } from '@programs'; import { AUDIT_AREA_SLIDES } from '@ui/tui/screens/audit/slides/index'; import type { AreaSlide } from '@ui/tui/screens/audit/slides/shared'; diff --git a/src/ui/tui/playground/demos/RunScreenDemo.tsx b/src/ui/tui/playground/demos/RunScreenDemo.tsx index d1e34c728..60a92516f 100644 --- a/src/ui/tui/playground/demos/RunScreenDemo.tsx +++ b/src/ui/tui/playground/demos/RunScreenDemo.tsx @@ -35,8 +35,8 @@ import type { ProgressItem, TabDefinition } from '@ui/tui/primitives/index'; import { LearnCard } from '@ui/tui/components/LearnCard'; import { TipsCard } from '@ui/tui/components/TipsCard'; import { VisualizerTab } from '@ui/tui/components/PhaseVisuals'; -import { getProgramConfig } from '@lib/programs/program-registry'; -import { getContentBlocks as getSkillContentBlocks } from '@lib/programs/agent-skill/content/index'; +import { getProgramConfig } from '@programs'; +import { getContentBlocks as getSkillContentBlocks } from '@ui/tui/decks/agent-skill/index'; import { Colors } from '@ui/tui/styles'; import { WIZARD_LOG_FILE } from '@utils/paths'; diff --git a/src/ui/tui/router.ts b/src/ui/tui/router.ts index 8c2dc9836..7bea7c35c 100644 --- a/src/ui/tui/router.ts +++ b/src/ui/tui/router.ts @@ -14,7 +14,7 @@ import { RunPhase, type WizardSession } from '@lib/wizard-session'; import { isRunFailure } from '@ui/mint-failure'; -import { Program, type ProgramId } from '@lib/programs/program-registry'; +import { Program, type ProgramId } from '@programs'; import { PROGRAM_SEQUENCES, MINT_HANDOFF_SEQUENCE, diff --git a/src/ui/tui/screen-registry.tsx b/src/ui/tui/screen-registry.tsx index 93e7dbdd3..c3b7d9e37 100644 --- a/src/ui/tui/screen-registry.tsx +++ b/src/ui/tui/screen-registry.tsx @@ -55,7 +55,7 @@ import { MintFailureScreen } from './screens/MintFailureScreen.js'; import type { MintFailureServices } from './screens/MintFailureScreen.js'; import { openCodingAgent } from './services/coding-agent-launcher.js'; import { writeWizardSpellbook } from '@lib/wizard-spellbook'; -import { getProgramConfig } from '@lib/programs/program-registry'; +import { getProgramConfig } from '@programs'; import { ExitScreen } from './screens/ExitScreen.js'; import { AuthErrorScreen } from './screens/AuthErrorScreen.js'; import { SessionTimeoutScreen } from './screens/SessionTimeoutScreen.js'; diff --git a/src/ui/tui/screen-sequences.ts b/src/ui/tui/screen-sequences.ts index 590468495..1ee869245 100644 --- a/src/ui/tui/screen-sequences.ts +++ b/src/ui/tui/screen-sequences.ts @@ -7,12 +7,9 @@ */ import type { WizardSession } from '@lib/wizard-session'; -import { - PROGRAM_REGISTRY, - type ProgramId, -} from '@lib/programs/program-registry'; -import { createProgramSequence } from '@lib/programs/program-step'; -import { withAiOptInGate } from '@lib/programs/ai-opt-in-gate'; +import { PROGRAM_REGISTRY, type ProgramId } from '@programs'; +import { createProgramSequence } from '@programs/program-step'; +import { withAiOptInGate } from '@programs/ai-opt-in-gate'; /** Screens that participate in linear programs. */ export enum ScreenId { diff --git a/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx b/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx index 9dd79815a..4cf416e1d 100644 --- a/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx +++ b/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx @@ -9,13 +9,13 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; import { detectErrorTrackingProjects, ERROR_TRACKING_PROJECT_PATH_KEY, type ErrorTrackingDetectionReport, type ErrorTrackingProject, -} from '@lib/programs/error-tracking/detect-agentic'; +} from '@programs/error-tracking/detect-agentic'; interface ErrorTrackingDetectScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx b/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx index c8d7ff9d7..1ebdddb0f 100644 --- a/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx +++ b/src/ui/tui/screens/McpSuggestedPromptsScreen.tsx @@ -43,7 +43,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { Colors, Icons } from '@ui/tui/styles'; import { useKeyBindings, KeyMatch } from '@ui/tui/hooks/useKeyBindings'; import { diff --git a/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx b/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx index e4331f473..09d933648 100644 --- a/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx +++ b/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx @@ -16,7 +16,7 @@ import { Integration } from '@shared/constants'; import { getCommandPath, getLaunchablePrograms, -} from '@lib/programs/program-registry'; +} from '@programs'; import { PickerMenu, LoadingBox, @@ -102,7 +102,7 @@ const FrameworkPicker = ({ options={options} onSelect={(value) => { const integration = Array.isArray(value) ? value[0] : value; - void import('@lib/registry').then(({ FRAMEWORK_REGISTRY }) => { + void import('@programs/registry').then(({ FRAMEWORK_REGISTRY }) => { const config = FRAMEWORK_REGISTRY[integration]; store.setFrameworkConfig(integration, config); store.setDetectedFramework(config.metadata.name); diff --git a/src/ui/tui/screens/RevenueIntroScreen.tsx b/src/ui/tui/screens/RevenueIntroScreen.tsx index 3d791dd3b..73e4f7ff0 100644 --- a/src/ui/tui/screens/RevenueIntroScreen.tsx +++ b/src/ui/tui/screens/RevenueIntroScreen.tsx @@ -18,7 +18,7 @@ import { POSTHOG_SDKS, STRIPE_SDKS, type RevenueDetectError, -} from '@lib/programs/revenue-analytics/index'; +} from '@programs/revenue-analytics/index'; interface RevenueIntroScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/RunScreen.tsx b/src/ui/tui/screens/RunScreen.tsx index 251ae5abd..db3cc8cd8 100644 --- a/src/ui/tui/screens/RunScreen.tsx +++ b/src/ui/tui/screens/RunScreen.tsx @@ -24,8 +24,8 @@ import { VisualizerTab } from '@ui/tui/components/PhaseVisuals'; import { TipsCard } from '@ui/tui/components/TipsCard'; import { useStdoutDimensions } from '@ui/tui/hooks/useStdoutDimensions'; -import { getProgramConfig } from '@lib/programs/program-registry'; -import { getContentBlocks as getSkillContentBlocks } from '@lib/programs/agent-skill/content/index'; +import { getProgramConfig } from '@programs'; +import { getContentBlocks as getSkillContentBlocks } from '@ui/tui/decks/agent-skill/index'; import { WIZARD_LOG_FILE } from '@utils/paths'; diff --git a/src/ui/tui/screens/SelfDrivingGitHubScreen.tsx b/src/ui/tui/screens/SelfDrivingGitHubScreen.tsx index d038f47e2..7bcc46e6c 100644 --- a/src/ui/tui/screens/SelfDrivingGitHubScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingGitHubScreen.tsx @@ -31,7 +31,7 @@ import { OutroKind } from '@lib/wizard-session'; import { GITHUB_REQUIRED_BODY, GITHUB_REQUIRED_MESSAGE, -} from '@lib/programs/self-driving/detect'; +} from '@programs/self-driving/detect'; import { analytics } from '@utils/analytics'; import { openTrackedLink } from '@utils/links'; import { getIntegrationAuthorizeUrl } from '@utils/urls'; diff --git a/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx b/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx index e3565bee6..06ba2c954 100644 --- a/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingHandoffScreen.tsx @@ -12,8 +12,8 @@ import { useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { PickerMenu } from '@ui/tui/primitives/index'; import { Colors } from '@ui/tui/styles'; -import { SETUP_REPORT_FILE } from '@lib/programs/posthog-integration/index'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { SETUP_REPORT_FILE } from '@programs/posthog-integration/index'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; interface SelfDrivingHandoffScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx index 110dbc8f0..6db94d501 100644 --- a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx @@ -15,13 +15,13 @@ import type { WizardStore } from '@ui/tui/store'; import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; import { detectSelfDrivingIntegrationProjects, type IntegrationProject, type IntegrationDetectionReport, -} from '@lib/programs/self-driving/detect-agentic'; +} from '@programs/self-driving/detect-agentic'; interface SelfDrivingIntegrationDetectScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/SelfDrivingIntroScreen.tsx b/src/ui/tui/screens/SelfDrivingIntroScreen.tsx index 94169aa01..99ed43f9d 100644 --- a/src/ui/tui/screens/SelfDrivingIntroScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingIntroScreen.tsx @@ -18,8 +18,8 @@ import { NO_DEFAULT_LIMIT, PRICING_LONG, PRICING_SHORT, -} from '@lib/programs/self-driving/content/pricing.js'; -import type { SelfDrivingDetectError } from '@lib/programs/self-driving/index'; +} from '@ui/tui/decks/self-driving/pricing.js'; +import type { SelfDrivingDetectError } from '@programs/self-driving/index'; interface SelfDrivingIntroScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/SetupScreen.tsx b/src/ui/tui/screens/SetupScreen.tsx index d6cce4502..959f82f91 100644 --- a/src/ui/tui/screens/SetupScreen.tsx +++ b/src/ui/tui/screens/SetupScreen.tsx @@ -12,7 +12,7 @@ import { useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { PickerMenu } from '@ui/tui/primitives/index'; import { Colors } from '@ui/tui/styles'; -import type { SetupQuestion } from '@lib/framework-config'; +import type { SetupQuestion } from '@programs/types'; interface SetupScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/SlackConnectScreen.tsx b/src/ui/tui/screens/SlackConnectScreen.tsx index ecc1958ba..423e551db 100644 --- a/src/ui/tui/screens/SlackConnectScreen.tsx +++ b/src/ui/tui/screens/SlackConnectScreen.tsx @@ -32,7 +32,7 @@ import { PickerMenu, LoadingBox } from '@ui/tui/primitives/index'; import { useKeyBindings, KeyMatch } from '@ui/tui/hooks/useKeyBindings'; import { getSlackAppCard } from '@lib/mcp-role-prompts'; import { fetchSlackConnected } from '@shared/api'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import { getOrAskForProjectData } from '@utils/setup-utils'; import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; diff --git a/src/ui/tui/screens/SourceMapsDetectScreen.tsx b/src/ui/tui/screens/SourceMapsDetectScreen.tsx index 699433e5f..645cd61cd 100644 --- a/src/ui/tui/screens/SourceMapsDetectScreen.tsx +++ b/src/ui/tui/screens/SourceMapsDetectScreen.tsx @@ -16,12 +16,12 @@ import { SOURCE_MAPS_CONTEXT_KEYS, VARIANT_DISPLAY_NAME, MANUAL_SDK_VARIANTS, -} from '@lib/programs/error-tracking-upload-source-maps/index'; +} from '@programs/error-tracking-upload-source-maps/index'; import { detectSourceMapsProjects, type DetectedProject, type DetectionReport, -} from '@lib/programs/error-tracking-upload-source-maps/detect-agentic'; +} from '@programs/error-tracking-upload-source-maps/detect-agentic'; interface SourceMapsDetectScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/WarehouseIntroScreen.tsx b/src/ui/tui/screens/WarehouseIntroScreen.tsx index 8a507c5c3..94205ef98 100644 --- a/src/ui/tui/screens/WarehouseIntroScreen.tsx +++ b/src/ui/tui/screens/WarehouseIntroScreen.tsx @@ -18,7 +18,7 @@ import { IntroScreenLayout } from './IntroScreenLayout.js'; import { getDetectedWarehouseSources, type WarehouseDetectError, -} from '@lib/programs/warehouse-source/index'; +} from '@programs/warehouse-source/index'; interface WarehouseIntroScreenProps { store: WizardStore; diff --git a/src/ui/tui/screens/audit/AuditAreaPane.tsx b/src/ui/tui/screens/audit/AuditAreaPane.tsx index 2ceb1e512..443d95c1e 100644 --- a/src/ui/tui/screens/audit/AuditAreaPane.tsx +++ b/src/ui/tui/screens/audit/AuditAreaPane.tsx @@ -17,7 +17,7 @@ import { Fragment } from 'react'; import { Box, Text, useInput } from 'ink'; import { spawn } from 'node:child_process'; import { Colors } from '@ui/tui/styles'; -import { type AuditCheck } from '@lib/programs/audit/types'; +import { type AuditCheck } from '@programs/audit/types'; import { AUDIT_AREA_SLIDES, type AreaSlide } from './slides/index.js'; // ── Helpers ────────────────────────────────────────────────────────── diff --git a/src/ui/tui/screens/audit/AuditChecksOutroSection.tsx b/src/ui/tui/screens/audit/AuditChecksOutroSection.tsx index 5d2af545a..fae95d0d0 100644 --- a/src/ui/tui/screens/audit/AuditChecksOutroSection.tsx +++ b/src/ui/tui/screens/audit/AuditChecksOutroSection.tsx @@ -2,7 +2,7 @@ import { Box, Text } from 'ink'; import { AUDIT_SEVERITY_STYLE, type AuditCheck, -} from '@lib/programs/audit/types'; +} from '@programs/audit/types'; import { relativeToInstallDir } from '@utils/paths'; interface AuditChecksOutroSectionProps { diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx index 25e887bd8..8c104eb83 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx +++ b/src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx @@ -23,7 +23,7 @@ import { useKeyBindings, type KeyBinding, } from '@ui/tui/hooks/useKeyBindings'; -import type { AuditCheck } from '@lib/programs/audit/types'; +import type { AuditCheck } from '@programs/audit/types'; import { AreaHeaderRow } from './AreaHeaderRow.js'; import { CheckRow } from './CheckRow.js'; import { DetailRow } from './DetailRow.js'; diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx index cb9b4c408..7506a87df 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx +++ b/src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx @@ -2,7 +2,7 @@ import { Box, Text } from 'ink'; import { AUDIT_SEVERITY_STYLE, type AuditCheck, -} from '@lib/programs/audit/types'; +} from '@programs/audit/types'; import { truncate, type ViewerLayout } from './layout.js'; interface CheckRowProps { diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx index 4b65b0e5d..84ae4304a 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx +++ b/src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx @@ -1,5 +1,5 @@ import { Box, Text } from 'ink'; -import type { AuditCheck } from '@lib/programs/audit/types'; +import type { AuditCheck } from '@programs/audit/types'; import type { ViewerLayout } from './layout.js'; interface DetailRowProps { diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx index dc01e3748..42b1bd6d3 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx +++ b/src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx @@ -1,6 +1,6 @@ import { Legend } from './Legend.js'; import { Summary } from './Header.js'; -import type { AuditStatus } from '@lib/programs/audit/types'; +import type { AuditStatus } from '@programs/audit/types'; interface FooterProps { total: number; diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx b/src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx index 51a97b24b..c8fe7c9fc 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx +++ b/src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx @@ -1,5 +1,5 @@ import { Box, Text } from 'ink'; -import type { AuditCheck, AuditStatus } from '@lib/programs/audit/types'; +import type { AuditCheck, AuditStatus } from '@programs/audit/types'; import type { ViewerLayout } from './layout.js'; interface HeaderProps { diff --git a/src/ui/tui/screens/audit/AuditChecksViewer/sort.ts b/src/ui/tui/screens/audit/AuditChecksViewer/sort.ts index 05406bd68..2d33264db 100644 --- a/src/ui/tui/screens/audit/AuditChecksViewer/sort.ts +++ b/src/ui/tui/screens/audit/AuditChecksViewer/sort.ts @@ -1,4 +1,4 @@ -import type { AuditCheck, AuditStatus } from '@lib/programs/audit/types'; +import type { AuditCheck, AuditStatus } from '@programs/audit/types'; const STATUS_ORDER: Record = { error: 0, diff --git a/src/ui/tui/screens/audit/AuditOutroScreen.tsx b/src/ui/tui/screens/audit/AuditOutroScreen.tsx index fd308d9f9..227af7968 100644 --- a/src/ui/tui/screens/audit/AuditOutroScreen.tsx +++ b/src/ui/tui/screens/audit/AuditOutroScreen.tsx @@ -11,7 +11,7 @@ import { useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { OutroKind } from '@lib/wizard-session'; import { Colors } from '@ui/tui/styles'; -import { getAuditChecks } from '@lib/programs/audit/types'; +import { getAuditChecks } from '@programs/audit/types'; import { AuditChecksOutroSection } from './AuditChecksOutroSection.js'; import { useDismissOnAnyKey } from '@ui/tui/hooks/useDismissOnAnyKey'; diff --git a/src/ui/tui/screens/audit/AuditRunScreen.tsx b/src/ui/tui/screens/audit/AuditRunScreen.tsx index b9cbc4f87..1ef1c0a46 100644 --- a/src/ui/tui/screens/audit/AuditRunScreen.tsx +++ b/src/ui/tui/screens/audit/AuditRunScreen.tsx @@ -13,8 +13,8 @@ import { AuditAreaPane } from './AuditAreaPane.js'; import { AUDIT_AREA_SLIDES } from './slides/index.js'; import { EVENTS_AUDIT_AREA_SLIDES } from './slides/events-audit/index.js'; import { PendingChecksList } from './PendingChecksList.js'; -import { AUDIT_REPORT_FILE, getAuditChecks } from '@lib/programs/audit/types'; -import { getProgramConfig } from '@lib/programs/program-registry'; +import { AUDIT_REPORT_FILE, getAuditChecks } from '@programs/audit/types'; +import { getProgramConfig } from '@programs'; import { WIZARD_LOG_FILE } from '@utils/paths'; interface AuditRunScreenProps { diff --git a/src/ui/tui/screens/audit/PendingChecksList.tsx b/src/ui/tui/screens/audit/PendingChecksList.tsx index 530dacafc..c6eb4f6f7 100644 --- a/src/ui/tui/screens/audit/PendingChecksList.tsx +++ b/src/ui/tui/screens/audit/PendingChecksList.tsx @@ -3,7 +3,7 @@ import { Spinner } from '@inkjs/ui'; import { AUDIT_SEVERITY_STYLE, type AuditCheck, -} from '@lib/programs/audit/types'; +} from '@programs/audit/types'; import { Colors, Icons } from '@ui/tui/styles'; import { LoadingBox } from '@ui/tui/primitives/index'; import { useStdoutDimensions } from '@ui/tui/hooks/useStdoutDimensions'; diff --git a/src/ui/tui/screens/doctor/DoctorReportScreen.tsx b/src/ui/tui/screens/doctor/DoctorReportScreen.tsx index 3e44e6d45..e090bd378 100644 --- a/src/ui/tui/screens/doctor/DoctorReportScreen.tsx +++ b/src/ui/tui/screens/doctor/DoctorReportScreen.tsx @@ -6,7 +6,7 @@ import { Colors, Icons } from '@ui/tui/styles'; import { fetchHealthIssues, type HealthIssue, -} from '@lib/programs/posthog-doctor/index'; +} from '@programs/posthog-doctor/index'; import { OutroKind } from '@lib/wizard-session'; import { ApiError } from '@shared/api'; import { POSTHOG_DOCS_URL } from '@shared/constants'; diff --git a/src/ui/tui/screens/doctor/IssueTable.tsx b/src/ui/tui/screens/doctor/IssueTable.tsx index d565f8005..f487a2ef6 100644 --- a/src/ui/tui/screens/doctor/IssueTable.tsx +++ b/src/ui/tui/screens/doctor/IssueTable.tsx @@ -5,7 +5,7 @@ import { getKindMeta, type HealthIssue, type HealthIssueSeverity, -} from '@lib/programs/posthog-doctor/index'; +} from '@programs/posthog-doctor/index'; export const SEVERITY_ORDER: HealthIssueSeverity[] = [ 'critical', diff --git a/src/ui/tui/services/mcp-suggested-prompts-services.ts b/src/ui/tui/services/mcp-suggested-prompts-services.ts index b92ad2d16..639a8de3d 100644 --- a/src/ui/tui/services/mcp-suggested-prompts-services.ts +++ b/src/ui/tui/services/mcp-suggested-prompts-services.ts @@ -12,7 +12,7 @@ import type { Credentials } from '@lib/wizard-session'; import { getOrAskForProjectData } from '@utils/setup-utils'; -import { Program } from '@lib/programs/program-registry'; +import { Program } from '@programs'; import type { WizardStore } from '@ui/tui/store'; import type { ApiUser } from '@shared/api'; import { @@ -108,7 +108,7 @@ export function createMcpSuggestedPromptsServices( // replays, errors, web/LLM analytics, cohorts, persons) plus // annotation read/write. Persistence writes (dashboard, insight, // notebook) come for free from the base set. See - // `src/lib/oauth/program-scopes.ts`. + // `src/programs/oauth/program-scopes.ts`. programId: Program.McpTutorial, }); return { diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts index 1f29dfef6..cbe3a8688 100644 --- a/src/ui/tui/store.ts +++ b/src/ui/tui/store.ts @@ -50,13 +50,10 @@ import { type ProgramId, } from './router.js'; import { analytics, sessionProperties } from '@utils/analytics'; -import type { - StoreInitContext, - ProgramReadyContext, -} from '@lib/programs/program-step'; -import { getProgramConfig } from '@lib/programs/program-registry'; -import { withAiOptInGate } from '@lib/programs/ai-opt-in-gate'; -import { reportWarehouseSourcesDetected } from '@lib/programs/posthog-integration/detect'; +import type { StoreInitContext, ProgramReadyContext } from '@programs/types'; +import { getProgramConfig } from '@programs'; +import { withAiOptInGate } from '@programs/ai-opt-in-gate'; +import { reportWarehouseSourcesDetected } from '@programs/posthog-integration/detect'; import { appendStatus } from '@shared/status-history'; import { IS_DEV } from '@shared/constants'; import { computeTokenCostUsd } from '@shared/token-pricing'; diff --git a/tsconfig.build.json b/tsconfig.build.json index d716e8367..e66f19f33 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -23,14 +23,16 @@ "@agent": ["./src/agent/index.ts"], "@agent/types": ["./src/agent/types.ts"], "@agent/*": ["./src/agent/*"], + "@programs": ["./src/programs/index.ts"], + "@programs/types": ["./src/programs/types.ts"], + "@programs/*": ["./src/programs/*"], "@lib/*": ["./src/lib/*"], "@e2e-harness/*": ["./e2e-harness/*"], "@utils/*": ["./src/shared/utils/*"], "@ui": ["./src/ui/index.ts"], "@ui/*": ["./src/ui/*"], "@steps": ["./src/steps/index.ts"], - "@steps/*": ["./src/steps/*"], - "@frameworks/*": ["./src/frameworks/*"] + "@steps/*": ["./src/steps/*"] } } } diff --git a/vitest.config.ts b/vitest.config.ts index 713b61ac1..16492b363 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -33,6 +33,7 @@ function resolveTsForJs(): Plugin { // alone with `vitest run --project `; `vitest run` runs them all. const TESTS = '__tests__/**/*.{js,jsx,ts,tsx}'; const AGENT_TESTS = [`src/agent/**/${TESTS}`]; +const PROGRAM_TESTS = [`src/programs/**/${TESTS}`]; const TUI_TESTS = [`src/ui/tui/**/${TESTS}`]; const CLI_TESTS = [ `src/commands/**/${TESTS}`, @@ -82,6 +83,9 @@ export default defineConfig({ { find: /^@agent$/, replacement: r('src/agent/index.ts') }, { find: /^@agent\/types$/, replacement: r('src/agent/types.ts') }, { find: /^@agent\/(.*)$/, replacement: `${r('src/agent')}/$1` }, + { find: /^@programs$/, replacement: r('src/programs/index.ts') }, + { find: /^@programs\/types$/, replacement: r('src/programs/types.ts') }, + { find: /^@programs\/(.*)$/, replacement: `${r('src/programs')}/$1` }, // Path aliases — mirror tsconfig `paths`. { find: /^@env$/, replacement: r('src/env.ts') }, { find: /^@lib\/(.*)$/, replacement: `${r('src/lib')}/$1` }, @@ -91,7 +95,6 @@ export default defineConfig({ { find: /^@ui\/(.*)$/, replacement: `${r('src/ui')}/$1` }, { find: /^@steps$/, replacement: r('src/steps/index.ts') }, { find: /^@steps\/(.*)$/, replacement: `${r('src/steps')}/$1` }, - { find: /^@frameworks\/(.*)$/, replacement: `${r('src/frameworks')}/$1` }, ], }, test: { @@ -99,6 +102,7 @@ export default defineConfig({ environment: 'node', projects: [ project('agent', AGENT_TESTS), + project('programs', PROGRAM_TESTS), project('tui', TUI_TESTS), project('cli', CLI_TESTS), project('harness', HARNESS_TESTS), @@ -108,7 +112,13 @@ export default defineConfig({ // The second glob keeps the pre-split behavior: a test file outside // a __tests__ directory still runs, here, rather than nowhere. [`src/**/${TESTS}`, 'src/**/*.{test,spec}.{js,jsx,ts,tsx}'], - [...AGENT_TESTS, ...TUI_TESTS, ...CLI_TESTS, ...ARCH_TESTS], + [ + ...AGENT_TESTS, + ...PROGRAM_TESTS, + ...TUI_TESTS, + ...CLI_TESTS, + ...ARCH_TESTS, + ], ), ], coverage: { From 2aee2503205354a26bc551d8b91331762873abf1 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 20:33:22 -0400 Subject: [PATCH 34/61] fix(agent): propagate terminal failures and cancel sibling work --- src/agent/README.md | 3 +- src/agent/__tests__/agent-interface.test.ts | 174 ++++++++++-- src/agent/__tests__/aio-capture.test.ts | 28 ++ .../__tests__/run-agent-standalone.test.ts | 103 ++++++- src/agent/__tests__/wizard-ask-bridge.test.ts | 68 +++++ src/agent/agent-interface.ts | 265 ++++++++++++++---- src/agent/aio-capture.ts | 14 +- src/agent/runner/README.md | 49 +++- src/agent/runner/harness/anthropic/index.ts | 4 +- .../harness/pi/__tests__/cancellation.test.ts | 42 +++ .../harness/pi/__tests__/gateway.test.ts | 79 ++++++ .../harness/pi/__tests__/mcp-env.test.ts | 43 +++ src/agent/runner/harness/pi/cancellation.ts | 28 ++ src/agent/runner/harness/pi/completion.ts | 4 +- src/agent/runner/harness/pi/gateway.ts | 65 ++++- src/agent/runner/harness/pi/index.ts | 140 ++++++++- src/agent/runner/harness/pi/mcp.ts | 90 ++++-- src/agent/runner/harness/pi/subagent.ts | 14 + src/agent/runner/harness/pi/task.ts | 129 ++++++++- src/agent/runner/harness/types.ts | 30 +- src/agent/runner/index.ts | 168 ++++++++--- src/agent/runner/sequence/linear.ts | 129 ++++----- .../orchestrator/__tests__/executor.test.ts | 56 +++- .../__tests__/task-notice-timeout.test.ts | 12 + .../runner/sequence/orchestrator/executor.ts | 37 ++- .../orchestrator/orchestrator-runner.ts | 262 ++++++++++++----- src/agent/runner/shared/ask.ts | 2 + src/agent/runner/shared/errors.ts | 25 +- src/agent/runner/shared/progress-collector.ts | 31 +- src/agent/runner/shared/types.ts | 11 +- src/agent/wizard-ask-bridge.ts | 29 +- .../__tests__/agentic-progress.test.ts | 39 ++- src/lib/detection/agentic.ts | 10 +- .../__tests__/run-agent-legacy.test.ts | 34 ++- src/lib/programs/run-agent-legacy.ts | 3 + 35 files changed, 1850 insertions(+), 370 deletions(-) create mode 100644 src/agent/runner/harness/pi/__tests__/cancellation.test.ts create mode 100644 src/agent/runner/harness/pi/__tests__/mcp-env.test.ts create mode 100644 src/agent/runner/harness/pi/cancellation.ts diff --git a/src/agent/README.md b/src/agent/README.md index 1c86b2e08..0d7de1ba7 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -13,6 +13,7 @@ import type { RunConfig, RunInput, RunResult, AgentProgress } from '@agent/types runAgent(config: RunConfig, input: RunInput, options?: { onProgress?: (event: AgentProgress) => void; interaction?: AgentInteraction; + signal?: AbortSignal; }): Promise ``` @@ -21,7 +22,7 @@ runAgent(config: RunConfig, input: RunInput, options?: { - `RunResult`: `outcome` is `RunOutcome.Success | Aborted | Failed | Crashed`. Success may carry an `outro`; the other three carry a `failure` (`AgentFailure`: message, outro data, error, exit code, error code, detail). Every result carries `skillId` and a `snapshot` of what the run reported: tasks, status lines, stage, token usage totals, final cost, dashboard and notebook URLs, handoff text. - `AgentProgress`: one event per thing the run reports, in emission order. Kinds: `lifecycle`, `spinner`, `log`, `status`, `tasks`, `stage`, `url`, `usage`, `finalCost`, `authError`, `handoff`, `completion`. Payloads are copies, never live objects. - `AgentInteraction`: every member optional. `ask(question)` resolves with answers, `cancelAsk()` dismisses the open question, `taskNotice(notice)` resolves with whether to keep an optional task, `cancelTaskNotice()` declines it. -- Errors: the agent does not exit the process and does not throw for a decided failure. An unexpected throw becomes `outcome: Crashed` with the error attached. A gateway 401 emits `authError` and then fails. +- Errors: the agent does not exit the process and returns decided failures. A caught coded error becomes `Failed`. An uncoded throw becomes `Crashed` with the error attached. A gateway 401 returns an auth failure. The host decides whether to show auth UI. Other runtime exports: `resolveBinding`, `shouldDisableAsk`, `initializeAgent`, `executeAgent`, `buildRunTags`, `AgentSignals`, `configureGatewayFromCIEnvironment`, `downloadSkill`, `WIZARD_TOOL_NAMES`, `LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and `runMcpPromptViaSdk`, which loads the streaming module on first call. diff --git a/src/agent/__tests__/agent-interface.test.ts b/src/agent/__tests__/agent-interface.test.ts index a1bec56b2..2b69ebc11 100644 --- a/src/agent/__tests__/agent-interface.test.ts +++ b/src/agent/__tests__/agent-interface.test.ts @@ -115,6 +115,133 @@ describe('runAgent', () => { }); describe('race condition handling', () => { + it('returns a failure for an SDK error result without an API marker', async () => { + function* failed() { + yield { + type: 'result', + subtype: 'error_max_turns', + is_error: true, + errors: ['maximum turns reached'], + }; + } + mockQuery.mockReturnValue(failed()); + const result = await runAgent( + defaultAgentConfig, + 'test prompt', + defaultOptions, + mockSpinner as unknown as SpinnerHandle, + ); + expect(result).toMatchObject({ + kind: 'failure', + classification: 'WIZARD_API_ERROR', + message: 'maximum turns reached', + }); + expect(mockSpinner.stop).not.toHaveBeenCalledWith('Test success'); + }); + + it('classifies an assistant structured 429 before a generic error result', async () => { + function* rateLimited() { + yield { + type: 'assistant', + error: { status: 429 }, + message: { role: 'assistant', content: [] }, + }; + yield { + type: 'result', + subtype: 'error_during_execution', + is_error: true, + errors: ['request failed'], + }; + } + mockQuery.mockReturnValue(rateLimited()); + const result = await runAgent( + defaultAgentConfig, + 'test prompt', + defaultOptions, + mockSpinner as unknown as SpinnerHandle, + ); + expect(result).toMatchObject({ + kind: 'failure', + classification: 'WIZARD_RATE_LIMIT', + }); + }); + + it('returns an abort when the host cancels and the SDK throws AbortError', async () => { + const controller = new AbortController(); + mockQuery.mockReturnValue({ + [Symbol.asyncIterator]() { + return { + next() { + controller.abort(); + return Promise.reject( + Object.assign(new Error('SDK aborted'), { + name: 'AbortError', + }), + ); + }, + }; + }, + }); + const result = await runAgent( + { ...defaultAgentConfig, signal: controller.signal }, + 'test prompt', + defaultOptions, + mockSpinner as unknown as SpinnerHandle, + ); + expect(result).toMatchObject({ + kind: 'abort', + classification: 'WIZARD_ABORT', + }); + }); + + it('returns a failure when the stream ends without a terminal result', async () => { + function* exhausted() { + yield { type: 'system', subtype: 'init', tools: [], mcp_servers: [] }; + } + mockQuery.mockReturnValue(exhausted()); + const result = await runAgent( + defaultAgentConfig, + 'test prompt', + defaultOptions, + mockSpinner as unknown as SpinnerHandle, + ); + expect(result).toMatchObject({ + kind: 'failure', + classification: 'WIZARD_API_ERROR', + }); + }); + + it('does not treat quoted API error prose as an auth failure', async () => { + function* quoted() { + yield { + type: 'assistant', + message: { + role: 'assistant', + content: [ + { + type: 'text', + text: 'API Error: 401 appears in the user log, but I fixed the issue.', + }, + ], + }, + }; + yield { + type: 'result', + subtype: 'success', + is_error: false, + result: 'done', + }; + } + mockQuery.mockReturnValue(quoted()); + const result = await runAgent( + defaultAgentConfig, + 'test prompt', + defaultOptions, + mockSpinner as unknown as SpinnerHandle, + ); + expect(result).toEqual({ kind: 'success' }); + }); + it('should return success when agent completes successfully then SDK cleanup fails', async () => { // This simulates the race condition: // 1. Agent completes with success result @@ -157,7 +284,7 @@ describe('runAgent', () => { ); // Should return success (empty object), not throw - expect(result).toEqual({}); + expect(result).toEqual({ kind: 'success' }); expect(mockSpinner.stop).toHaveBeenCalledWith('Test success'); }); @@ -232,8 +359,13 @@ describe('runAgent', () => { ); // Should return API error, not success - expect(result.error).toBe('WIZARD_API_ERROR'); - expect(result.message).toContain('API Error'); + expect(result.kind).toBe('failure'); + expect(result.kind === 'failure' && result.classification).toBe( + 'WIZARD_API_ERROR', + ); + expect(result.kind === 'failure' && result.message).toContain( + 'API Error', + ); }); it('should suppress user-facing errors when SDK yields error result after success', async () => { @@ -297,7 +429,7 @@ describe('runAgent', () => { ); // Should return success (empty object), not error - expect(result).toEqual({}); + expect(result).toEqual({ kind: 'success' }); expect(mockSpinner.stop).toHaveBeenCalledWith('Test success'); // ui.log.error should NOT have been called (errors suppressed for user) @@ -354,7 +486,7 @@ describe('runAgent', () => { }, ); - expect(result).toEqual({}); + expect(result).toEqual({ kind: 'success' }); expect(mockSpinner.stop).toHaveBeenCalledWith('Test success'); expect(mockUIInstance.log.error).not.toHaveBeenCalled(); }); @@ -394,7 +526,7 @@ describe('runAgent', () => { }, ); - expect(result).toEqual({}); + expect(result).toEqual({ kind: 'success' }); expect(mockSpinner.stop).toHaveBeenCalledWith('Test success'); }); }); @@ -740,6 +872,7 @@ describe('gateway re-mint on 401', () => { yield { type: 'assistant', session_id: id, + error: { status: 401 }, message: { role: 'assistant', content: [ @@ -791,7 +924,7 @@ describe('gateway re-mint on 401', () => { const result = await run(cfg); - expect(result).toEqual({}); + expect(result).toEqual({ kind: 'success' }); expect(refresh).toHaveBeenCalledTimes(1); expect(authErrors()).toHaveLength(0); expect(mockQuery).toHaveBeenCalledTimes(2); @@ -832,12 +965,16 @@ describe('gateway re-mint on 401', () => { expect(refresh).toHaveBeenCalledTimes(1); expect(mockQuery).toHaveBeenCalledTimes(2); - expect(authErrors()).toHaveLength(1); + expect(authErrors()).toHaveLength(0); // The auth screen is reported, and the decided failure goes back to the // caller, which owns the exit. - expect(result.error).toBeUndefined(); - expect(result.failure?.message).toBe('Authentication failed (401)'); - expect(result.failure?.code).toBeDefined(); + expect(result.kind).toBe('decided_failure'); + expect(result.kind === 'decided_failure' && result.failure.message).toBe( + 'Authentication failed (401)', + ); + expect( + result.kind === 'decided_failure' && result.failure.code, + ).toBeDefined(); }); it('judges a failed resumed session on its own error, not the old 401', async () => { @@ -871,9 +1008,12 @@ describe('gateway re-mint on 401', () => { // The 401 that triggered the re-mint is history; reporting it here would // send the user to the auth screen for a 500. - expect(result.error).toBe('WIZARD_API_ERROR'); - expect(result.message).toContain('500'); - expect(result.message).not.toContain('401'); + expect(result.kind).toBe('failure'); + expect(result.kind === 'failure' && result.classification).toBe( + 'WIZARD_API_ERROR', + ); + expect(result.kind === 'failure' && result.message).toContain('500'); + expect(result.kind === 'failure' && result.message).not.toContain('401'); expect(authErrors()).toHaveLength(0); }); @@ -888,8 +1028,10 @@ describe('gateway re-mint on 401', () => { // A fresh token the gateway rejects is a bad credential, not age. expect(refresh).not.toHaveBeenCalled(); expect(mockQuery).toHaveBeenCalledTimes(1); - expect(authErrors()).toHaveLength(1); - expect(result.failure?.message).toBe('Authentication failed (401)'); + expect(authErrors()).toHaveLength(0); + expect(result.kind === 'decided_failure' && result.failure.message).toBe( + 'Authentication failed (401)', + ); }); }); diff --git a/src/agent/__tests__/aio-capture.test.ts b/src/agent/__tests__/aio-capture.test.ts index 5d0c8d90b..f949a7ec9 100644 --- a/src/agent/__tests__/aio-capture.test.ts +++ b/src/agent/__tests__/aio-capture.test.ts @@ -388,6 +388,7 @@ describe('createAioCapture', () => { type: 'agent_end', willRetry: false, }); + capture.finishPiRun(false); await flushMicrotasks(); const traceBody = JSON.parse(fetchMock.mock.calls[0][1].body); @@ -395,6 +396,33 @@ describe('createAioCapture', () => { expect(traceBody.properties.$ai_span_name).toBe('posthog-integration'); }); + it('marks a failed Pi final turn and trace as errors', async () => { + const capture = createAioCapture(BASE_ARGS); + capture.captureFromPiMessageEndEvent({ + type: 'message_end', + message: { + role: 'assistant', + model: 'claude-sonnet-4-5', + content: [], + stopReason: 'error', + errorMessage: '429 rate limited', + }, + }); + capture.finishPiRun(true); + await flushMicrotasks(); + const bodies = fetchMock.mock.calls.map((call) => + JSON.parse(call[1].body), + ); + expect( + bodies.find((body) => body.event === '$ai_generation').properties + .$ai_is_error, + ).toBe(true); + expect( + bodies.find((body) => body.event === '$ai_trace').properties + .$ai_is_error, + ).toBe(true); + }); + it('does not emit $ai_trace on pi agent_end with willRetry=true', async () => { const capture = createAioCapture(BASE_ARGS); capture.captureFromPiMessageEndEvent({ diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index d03bd9890..cb26b03d2 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -16,11 +16,12 @@ import { type AskAnswers, type PendingQuestion, } from '@lib/wizard-session'; -import { AGENT_ERROR_CODE, ErrorCodes } from '@shared/errors'; +import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@shared/errors'; import { AgentErrorType } from '@agent/signals'; import type { AgentFailure } from '@agent/runner/shared/types'; import type { AgentProgress } from '@agent/progress'; import type { + AgentResult, AgentHarness, BackendRunInputs, TaskRunInputs, @@ -64,12 +65,13 @@ vi.mock('@agent/gateway-session', async (importOriginal) => ({ // The fake harness: reports a little of everything, then returns what the // current test told it to. const harnessState = vi.hoisted(() => ({ - result: {} as { error?: string; message?: string; failure?: unknown }, + result: { kind: 'success' } as AgentResult, throws: undefined as Error | undefined, lastInputs: undefined as unknown, tasks: [] as TaskRunInputs[], selected: [] as Harness[], taskFailure: undefined as AgentFailure | undefined, + taskThrow: undefined as Error | undefined, seedFailure: undefined as AgentFailure | undefined, askQuestions: undefined as PendingQuestion['questions'] | undefined, })); @@ -91,10 +93,12 @@ vi.mock('@agent/runner/switchboard/harness', () => { const { store, currentTaskId } = inputs.orchestrator; if (!currentTaskId) { if (harnessState.seedFailure) - return Promise.resolve({ failure: harnessState.seedFailure }); + return { kind: 'decided_failure', failure: harnessState.seedFailure }; store.enqueue({ type: 'install' }); + } else if (harnessState.taskThrow) { + throw harnessState.taskThrow; } else if (harnessState.taskFailure) { - return Promise.resolve({ failure: harnessState.taskFailure }); + return { kind: 'decided_failure', failure: harnessState.taskFailure }; } else { await askIfRequested(inputs); store.complete(currentTaskId, { @@ -103,7 +107,7 @@ vi.mock('@agent/runner/switchboard/harness', () => { forNextAgent: 'done', }); } - return Promise.resolve({}); + return { kind: 'success' }; }, async run(inputs: BackendRunInputs) { harnessState.lastInputs = inputs; @@ -133,7 +137,7 @@ vi.mock('@agent/runner/switchboard/harness', () => { await askIfRequested(inputs); if (harnessState.throws) throw harnessState.throws; spinner.stop('Done'); - return harnessState.result as never; + return harnessState.result; }, }; return { @@ -243,10 +247,11 @@ const input = (over: Partial = {}): RunInput => ({ beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'run-agent-standalone-')); - harnessState.result = {}; + harnessState.result = { kind: 'success' }; harnessState.tasks = []; harnessState.selected = []; harnessState.taskFailure = undefined; + harnessState.taskThrow = undefined; harnessState.seedFailure = undefined; harnessState.throws = undefined; harnessState.lastInputs = undefined; @@ -381,7 +386,10 @@ describe('runAgent standalone', () => { ); it('cleans up when the seed fails before the drain starts', async () => { - const failure = { message: 'Authentication failed (401)' }; + const failure = { + code: ErrorCodes.AgentApiError, + message: 'Authentication failed (401)', + }; harnessState.seedFailure = failure; const result = await runAgent( config({ @@ -431,6 +439,38 @@ describe('runAgent standalone', () => { expect(fs.existsSync(path.join(tmp, QUEUE_DIR_NAME))).toBe(false); }); + it('treats a coded task harness rejection as run-fatal without retrying', async () => { + const error = new WizardError( + 'Gateway rejected the request', + {}, + ErrorCodes.AgentApiError, + ); + harnessState.taskThrow = error; + const result = await runAgent( + config({ + binding: { + harness: Harness.pi, + sequence: Sequence.orchestrator, + model: DEFAULT_AGENT_MODEL, + }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.pi, + }, + }), + input(), + ); + expect(result.outcome).toBe('failed'); + expect(result.failure).toMatchObject({ + code: ErrorCodes.AgentApiError, + message: error.message, + }); + expect(result.failure?.error).toBe(error); + expect(harnessState.tasks).toHaveLength(2); + expect(fs.existsSync(path.join(tmp, QUEUE_DIR_NAME))).toBe(false); + }); + it.each([Sequence.linear, Sequence.orchestrator])( 'preserves %s completion, shutdown and scan-flush ordering', async (sequence) => { @@ -571,7 +611,8 @@ describe('runAgent standalone', () => { it('returns an agent abort as a decided failure with the matched case', async () => { harnessState.result = { - error: AgentErrorType.ABORT, + kind: 'abort', + classification: AgentErrorType.ABORT, message: 'No Stripe found', }; const events: AgentProgress[] = []; @@ -588,15 +629,16 @@ describe('runAgent standalone', () => { body: 'Stripe is required.', errorDetail: { reason: 'No Stripe found' }, }); - expect(result.failure?.error?.message).toBe( - 'Agent aborted: No Stripe found', - ); + expect(result.failure?.error).toBeUndefined(); expect(events.some((e) => e.kind === 'completion')).toBe(false); expect(analytics.shutdown).not.toHaveBeenCalled(); }); it('returns a coded failure for a harness error', async () => { - harnessState.result = { error: AgentErrorType.NO_PROGRESS }; + harnessState.result = { + kind: 'failure', + classification: AgentErrorType.NO_PROGRESS, + }; const result = await runAgent(config(), input()); @@ -607,9 +649,23 @@ describe('runAgent standalone', () => { expect(result.failure?.message).toContain('without changing your project'); }); + it('resolves a crash even when the thrown Error has hostile getters', async () => { + const hostile = new Error('hidden'); + Object.defineProperty(hostile, 'message', { + get() { + throw new Error('message getter'); + }, + }); + harnessState.throws = hostile; + const result = await runAgent(config(), input()); + expect(result.outcome).toBe(RunOutcome.Crashed); + expect(result.failure?.error).toBe(hostile); + expect(result.failure?.code).toBe(ErrorCodes.InternalUnhandled); + }); + it('passes a harness-decided failure through untouched', async () => { const failure = { code: ErrorCodes.AgentAbort, message: 'decided' }; - harnessState.result = { failure }; + harnessState.result = { kind: 'decided_failure', failure }; const result = await runAgent(config(), input()); @@ -644,6 +700,25 @@ describe('runAgent standalone', () => { expect(result.snapshot.tasks).toHaveLength(1); }); + it('finishes when an async observer rejects', async () => { + const result = await runAgent(config(), input(), { + onProgress: () => Promise.reject(new Error('observer rejected')), + }); + expect(result.outcome).toBe(RunOutcome.Success); + expect(result.snapshot.tasks).toHaveLength(1); + }); + + it('returns an aborted result before setup for a pre-aborted host signal', async () => { + const controller = new AbortController(); + controller.abort(); + const result = await runAgent(config(), input(), { + signal: controller.signal, + }); + expect(result.outcome).toBe(RunOutcome.Aborted); + expect(result.failure?.code).toBe(ErrorCodes.AgentAbort); + expect(harnessState.lastInputs).toBeUndefined(); + }); + it('calls the bound hooks with the run credentials', async () => { const postRun = vi.fn().mockResolvedValue(undefined); const buildOutroData = vi.fn(() => ({ diff --git a/src/agent/__tests__/wizard-ask-bridge.test.ts b/src/agent/__tests__/wizard-ask-bridge.test.ts index 0f348ab17..d17a33a3d 100644 --- a/src/agent/__tests__/wizard-ask-bridge.test.ts +++ b/src/agent/__tests__/wizard-ask-bridge.test.ts @@ -19,6 +19,49 @@ beforeEach(() => { }); describe('createWizardAskBridge', () => { + it('dismisses and settles an active question on run cancellation', async () => { + const controller = new AbortController(); + const cancelQuestion = vi.fn(); + const bridge = createWizardAskBridge({ + signal: controller.signal, + getSource: () => 'skill', + showQuestion: () => new Promise(() => undefined), + cancelQuestion, + }); + const result = bridge.request({ + questions: [{ id: 'goal', prompt: 'Goal?', kind: 'text' }], + }); + expect(bridge.getPendingQuestion()).not.toBeNull(); + controller.abort(); + await expect(result).resolves.toEqual({ + answers: { goal: CANCELLED_SENTINEL }, + timedOut: false, + }); + expect(cancelQuestion).toHaveBeenCalledTimes(1); + expect(bridge.getPendingQuestion()).toBeNull(); + }); + + it('does not open a question when the run was already cancelled', async () => { + const controller = new AbortController(); + controller.abort(); + const showQuestion = vi.fn(); + const bridge = createWizardAskBridge({ + signal: controller.signal, + getSource: () => 'skill', + showQuestion, + }); + await expect( + bridge.request({ + questions: [{ id: 'goal', prompt: 'Goal?', kind: 'text' }], + }), + ).resolves.toEqual({ + answers: { goal: CANCELLED_SENTINEL }, + timedOut: false, + }); + expect(showQuestion).not.toHaveBeenCalled(); + expect(bridge.getPendingQuestion()).toBeNull(); + }); + it('forwards questions to showQuestion and resolves with the captured answers', async () => { const captured: PendingQuestion[] = []; let resolveAnswers!: (answers: AskAnswers) => void; @@ -255,6 +298,31 @@ describe('createWizardAskBridge', () => { } }); + it('settles a timed-out question when host dismissal throws', async () => { + vi.useFakeTimers(); + try { + const bridge = createWizardAskBridge({ + getSource: () => 'product-tours', + showQuestion: () => new Promise(() => undefined), + cancelQuestion: () => { + throw new Error('overlay broken'); + }, + timeoutMs: 1000, + }); + const result = bridge.request({ + questions: [{ id: 'goal', prompt: 'Goal?', kind: 'text' }], + }); + vi.advanceTimersByTime(1000); + await expect(result).resolves.toEqual({ + answers: { goal: CANCELLED_SENTINEL }, + timedOut: true, + }); + expect(bridge.getPendingQuestion()).toBeNull(); + } finally { + vi.useRealTimers(); + } + }); + it('does not dismiss the overlay when the user answers before the timeout', async () => { vi.useFakeTimers(); try { diff --git a/src/agent/agent-interface.ts b/src/agent/agent-interface.ts index c418e5c19..a118b29ea 100644 --- a/src/agent/agent-interface.ts +++ b/src/agent/agent-interface.ts @@ -29,6 +29,7 @@ import { ADDITIONAL_FEATURE_PROMPTS, } from '@shared/constants'; import type { AgentFailure } from './runner/shared/types'; +import type { AgentResult } from './runner/harness/types'; import { createCustomHeaders } from '@utils/custom-headers'; import type { HostResolution } from '@shared/host-resolution'; import { @@ -55,7 +56,7 @@ import { REMARK_INSTRUCTION, RESUME_INSTRUCTION, } from './signals'; -import { classifyAuthFailure, WizardError } from '@shared/errors'; +import { classifyAuthFailure } from '@shared/errors'; import { isGrantRevoked } from '@shared/auth-session-state'; import { AgentOutputSignals } from './output-signals'; @@ -318,6 +319,7 @@ export function createStopHook( * Internal configuration object returned by initializeAgent */ type AgentRunConfig = { + signal?: AbortSignal; workingDirectory: string; mcpServers: McpServersConfig; model: string; @@ -716,6 +718,52 @@ export async function initializeAgent( * * @returns An object containing any error detected in the agent's output */ +function sdkErrorStatus(value: unknown): number | undefined { + if (typeof value === 'number' && value >= 400 && value < 600) return value; + if (typeof value === 'string') { + const match = value.match(/(?:^|\b)(4\d\d|5\d\d)(?:\b|$)/); + return match ? Number(match[1]) : undefined; + } + if (value && typeof value === 'object') { + const record = value as Record; + return ( + sdkErrorStatus(record.status) ?? + sdkErrorStatus(record.statusCode) ?? + sdkErrorStatus(record.code) ?? + sdkErrorStatus(record.error) + ); + } + return undefined; +} + +function sdkResultFailure( + message: Record, +): Extract | undefined { + if (message.subtype === 'success' && message.is_error !== true) + return undefined; + const errors = Array.isArray(message.errors) ? message.errors : []; + const detail = + errors + .map((error) => + typeof error === 'string' ? error : JSON.stringify(error), + ) + .join('; ') || + (typeof message.result === 'string' + ? message.result + : String(message.subtype ?? 'SDK result failed')); + const status = + sdkErrorStatus(message.api_error_status) ?? + sdkErrorStatus(message.status) ?? + errors.map(sdkErrorStatus).find((code) => code !== undefined) ?? + sdkErrorStatus(message.result); + return { + kind: 'failure', + classification: + status === 429 ? AgentErrorType.RATE_LIMIT : AgentErrorType.API_ERROR, + message: detail, + }; +} + export async function runAgent( agentConfig: AgentRunConfig, prompt: string, @@ -752,11 +800,14 @@ export async function runAgent( onMessage(message: any): void; finalize(resultMessage: any, totalDurationMs: number): any; }, -): Promise<{ - error?: AgentErrorType; - message?: string; - failure?: AgentFailure; -}> { +): Promise { + if (agentConfig.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } const emit = agentConfig.emit ?? NO_PROGRESS; const { spinnerMessage = 'Customizing your PostHog setup...', @@ -783,6 +834,8 @@ export async function runAgent( let receivedSuccessResult = false; let loggedInitialContext = false; let lastResultMessage: any = null; + let terminalFailure: Extract | undefined; + let assistantErrorStatus: number | undefined; // SDK >=0.3.142 replaced TodoWrite (snapshot) with TaskCreate/TaskUpdate (accumulate by id). // The agent's TaskCreate tool_use doesn't know the assigned taskId — the SDK returns it @@ -814,9 +867,7 @@ export async function runAgent( }; // Helper to handle successful completion (used in normal path and race condition recovery) - const completeWithSuccess = ( - suppressedError?: Error, - ): { error?: AgentErrorType; message?: string } => { + const completeWithSuccess = (suppressedError?: Error): AgentResult => { const durationMs = Date.now() - startTime; const durationSeconds = Math.round(durationMs / 1000); @@ -879,13 +930,17 @@ export async function runAgent( logToFile(`${AgentSignals.BENCHMARK} Middleware finalize error:`, e); } spinner.stop(successMessage); - return {}; + return { kind: 'success' }; }; // Abort controller — lets us force-kill the SDK query when we detect an // [ABORT] signal in the agent's output. Also stashes the reason so the // runner can surface it via outroData after we unwind. let abortController = new AbortController(); + const onExternalAbort = () => { + abortController.abort(); + signalDone(); + }; let abortReason: string | null = null; // Set when a YARA hook detects a terminal violation. Returning `stopReason` // from a PostToolUse hook does NOT stop the SDK, so we abort the query and @@ -900,6 +955,10 @@ export async function runAgent( // failure the caller ends the run with. The query is aborted to unwind. let authFailure: AgentFailure | undefined; const agentConfigDir = createIsolatedAgentConfigDir(); + agentConfig.signal?.addEventListener('abort', onExternalAbort, { + once: true, + }); + if (agentConfig.signal?.aborted) onExternalAbort(); try { // Per-program allow/disallow lists tweak BASE_ALLOWED_TOOLS. Skills are @@ -1189,6 +1248,10 @@ export async function runAgent( emitStepEvents, resolveStepKey, ); + if (message.type === 'assistant') { + assistantErrorStatus = + sdkErrorStatus(message.error) ?? assistantErrorStatus; + } // [ABORT] detection: the skill emits "[ABORT] " when it // cannot complete the program. Kill the SDK query immediately — @@ -1220,12 +1283,24 @@ export async function runAgent( // 401 on a bearer past its refresh instant: it aged out, so re-mint // once and resume. Any other 401 is a bad credential: show the auth // error screen and exit. - if (message.type === 'assistant' && signals.hasApiErrorStatus(401)) { + if ( + (message.type === 'assistant' && + sdkErrorStatus(message.error) === 401) || + (message.type === 'result' && + (sdkErrorStatus(message.api_error_status) === 401 || + sdkErrorStatus(message.status) === 401)) || + (message.type === 'result' && + Array.isArray(message.errors) && + message.errors.some( + (error: unknown) => sdkErrorStatus(error) === 401, + )) + ) { signalDone(); if ( agentConfig.refreshGatewayAuth && !reminted && - isPastRefresh(agentConfig.gatewayAuth) + isPastRefresh(agentConfig.gatewayAuth) && + !agentConfig.signal?.aborted ) { logToFile( 'Agent error: 401 on an aged gateway bearer; re-minting', @@ -1261,9 +1336,12 @@ export async function runAgent( ...authError, sessionExpired, }); - emit({ - kind: 'authError', - detail: { + // The caller ends the run with this; the query is abandoned here + // where the process used to exit. + authFailure = { + code: authCode, + message: 'Authentication failed (401)', + authErrorDetail: { hasSettingsConflict: authError.hasSettingsConflict, conflicts: authError.conflicts, usingManagedLogin: authError.usingManagedLogin, @@ -1271,25 +1349,19 @@ export async function runAgent( sessionExpired, logFilePath: getLogFilePath(), }, - }); - // The caller ends the run with this; the query is abandoned here - // where the process used to exit. - authFailure = { - code: authCode, - message: 'Authentication failed (401)', - error: new WizardError( - 'Authentication failed', - { - hasSettingsConflict: authError.hasSettingsConflict, - conflictSources: authError.conflictSources, - conflictKeys: authError.conflictKeys, - gatewayUrl: authError.gatewayUrl, - region: authError.region, - usingManagedLogin: authError.usingManagedLogin, - apiKeySource: authError.apiKeySource, - }, - authCode, - ), + detail: { + hasSettingsConflict: authError.hasSettingsConflict, + conflictSources: authError.conflictSources, + conflictKeys: authError.conflictKeys, + conflicts: authError.conflicts, + credentialPlaces: authError.credentialPlaces, + sessionExpired, + logFilePath: getLogFilePath(), + gatewayUrl: authError.gatewayUrl, + region: authError.region, + usingManagedLogin: authError.usingManagedLogin, + apiKeySource: authError.apiKeySource, + }, }; abortController.abort(); break; @@ -1311,6 +1383,14 @@ export async function runAgent( if (message.subtype === 'success' && !message.is_error) { receivedSuccessResult = true; lastResultMessage = message; + } else if (!receivedSuccessResult) { + terminalFailure = sdkResultFailure(message); + if (terminalFailure && assistantErrorStatus === 429) { + terminalFailure = { + ...terminalFailure, + classification: AgentErrorType.RATE_LIMIT, + }; + } } signalDone(); } @@ -1325,17 +1405,28 @@ export async function runAgent( }; const refreshGatewayAuth = agentConfig.refreshGatewayAuth; - if ((await runQuery()) === 'remint' && refreshGatewayAuth) { + if ( + (await runQuery()) === 'remint' && + refreshGatewayAuth && + !agentConfig.signal?.aborted + ) { // The subprocess froze the dead bearer in its env at spawn, so it cannot // be handed a new one: mint, then resume the session in a new one. reminted = true; remintRequested = false; abortController = new AbortController(); + if (agentConfig.signal?.aborted) abortController.abort(); signals.forgetApiErrors(); spinner.message('Renewing the gateway token...'); const stale = agentConfig.gatewayAuth; // A refusal or failure here ends the run with its own message. agentConfig.gatewayAuth = await refreshGatewayAuth(); + if (agentConfig.signal?.aborted) + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; logToFile( `Gateway token renewed after a 401 (${Math.round( (Date.now() - stale.refreshAtMs) / 1000, @@ -1353,34 +1444,56 @@ export async function runAgent( // A fresh bearer was rejected. The auth screen is already up; hand the // decided failure to the caller, which owns the exit. if (authFailure) { - return { failure: authFailure }; + return { kind: 'decided_failure', failure: authFailure }; + } + if (agentConfig.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; } // A YARA hook detected a terminal violation and aborted the run. if (yaraViolationReason) { logToFile('Agent error: YARA_VIOLATION'); spinner.stop('Security check stopped the setup'); - return { error: AgentErrorType.YARA_VIOLATION }; + return { kind: 'failure', classification: AgentErrorType.YARA_VIOLATION }; } // If the middleware caught an [ABORT] and aborted the SDK query, surface // it as a structured error before checking other signals. if (abortReason) { spinner.stop('Wizard aborted'); - return { error: AgentErrorType.ABORT, message: abortReason }; + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: abortReason, + }; + } + if (agentConfig.signal?.aborted) { + spinner.stop('Wizard aborted'); + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; } // Check for error markers in the agent's output if (signals.has('MCP_MISSING')) { logToFile('Agent error: MCP_MISSING'); spinner.stop('Agent could not access PostHog MCP'); - return { error: AgentErrorType.MCP_MISSING }; + return { kind: 'failure', classification: AgentErrorType.MCP_MISSING }; } if (signals.has('RESOURCE_MISSING')) { logToFile('Agent error: RESOURCE_MISSING'); spinner.stop('Agent could not access setup resource'); - return { error: AgentErrorType.RESOURCE_MISSING }; + return { + kind: 'failure', + classification: AgentErrorType.RESOURCE_MISSING, + }; } // A clean success result already arrived. The Claude SDK can emit a second @@ -1393,6 +1506,11 @@ export async function runAgent( return completeWithSuccess(); } + if (terminalFailure) { + spinner.stop(errorMessage); + return terminalFailure; + } + // Check for API errors (rate limits, etc.) // Surface just the API error line(s), not the entire output const apiErrorMessage = signals.apiErrorMessage() ?? 'Unknown API error'; @@ -1400,16 +1518,29 @@ export async function runAgent( if (signals.hasApiErrorStatus(429)) { logToFile('Agent error: RATE_LIMIT'); spinner.stop('Rate limit exceeded'); - return { error: AgentErrorType.RATE_LIMIT, message: apiErrorMessage }; + return { + kind: 'failure', + classification: AgentErrorType.RATE_LIMIT, + message: apiErrorMessage, + }; } if (signals.hasApiError()) { logToFile('Agent error: API_ERROR'); spinner.stop('API error occurred'); - return { error: AgentErrorType.API_ERROR, message: apiErrorMessage }; + return { + kind: 'failure', + classification: AgentErrorType.API_ERROR, + message: apiErrorMessage, + }; } - return completeWithSuccess(); + spinner.stop(errorMessage); + return { + kind: 'failure', + classification: AgentErrorType.API_ERROR, + message: 'SDK stream ended without a success result', + }; } catch (error) { // Signal done to unblock the async generator signalDone(); @@ -1420,14 +1551,27 @@ export async function runAgent( if (yaraViolationReason) { logToFile('Agent error: YARA_VIOLATION'); spinner.stop('Security check stopped the setup'); - return { error: AgentErrorType.YARA_VIOLATION }; + return { kind: 'failure', classification: AgentErrorType.YARA_VIOLATION }; } // If the middleware caught an [ABORT] and triggered abortController.abort(), // the SDK will throw an AbortError — surface it as a clean abort result. if (abortReason) { spinner.stop('Wizard aborted'); - return { error: AgentErrorType.ABORT, message: abortReason }; + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: abortReason, + }; + } + + if (agentConfig.signal?.aborted) { + spinner.stop('Wizard aborted'); + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; } // If we already received a successful result, the error is from SDK cleanup @@ -1445,13 +1589,23 @@ export async function runAgent( if (signals.hasApiErrorStatus(429)) { logToFile('Agent error (caught): RATE_LIMIT'); spinner.stop('Rate limit exceeded'); - return { error: AgentErrorType.RATE_LIMIT, message: apiErrorMessage }; + return { + kind: 'failure', + classification: AgentErrorType.RATE_LIMIT, + message: apiErrorMessage, + error: error instanceof Error ? error : undefined, + }; } if (signals.hasApiError()) { logToFile('Agent error (caught): API_ERROR'); spinner.stop('API error occurred'); - return { error: AgentErrorType.API_ERROR, message: apiErrorMessage }; + return { + kind: 'failure', + classification: AgentErrorType.API_ERROR, + message: apiErrorMessage, + error: error instanceof Error ? error : undefined, + }; } // No API error found, re-throw the original exception @@ -1465,17 +1619,22 @@ export async function runAgent( debug('Full error:', error); throw error; } finally { + agentConfig.signal?.removeEventListener('abort', onExternalAbort); // Always capture run duration, even on abort/error, so we can alert on // long runs where the user gave up before completion. A 401 never reached // this block before (the process exited first), so it still does not count. if (!receivedSuccessResult && !authFailure) { const durationMs = Date.now() - startTime; - analytics.wizardCapture('agent aborted', { - duration_ms: durationMs, - duration_seconds: Math.round(durationMs / 1000), - model: agentConfig.model, - ...config?.analyticsProperties, - }); + try { + analytics.wizardCapture('agent aborted', { + duration_ms: durationMs, + duration_seconds: Math.round(durationMs / 1000), + model: agentConfig.model, + ...config?.analyticsProperties, + }); + } catch { + // Analytics must not replace the SDK outcome. + } } } } diff --git a/src/agent/aio-capture.ts b/src/agent/aio-capture.ts index be4182c3c..f9812f849 100644 --- a/src/agent/aio-capture.ts +++ b/src/agent/aio-capture.ts @@ -62,6 +62,7 @@ export type AioCapture = { captureFromAnthropicSDKMessage(msg: unknown): void; /** Called for every subscribe event on the pi harness. */ captureFromPiMessageEndEvent(event: unknown): void; + finishPiRun(isError: boolean): void; }; const noop = (_: unknown): void => undefined; @@ -69,6 +70,7 @@ const NOOP: AioCapture = { setInitialPrompt: noop, captureFromAnthropicSDKMessage: noop, captureFromPiMessageEndEvent: noop, + finishPiRun: noop, }; export function createAioCapture(args: { @@ -216,6 +218,7 @@ export function createAioCapture(args: { role?: string; model?: string; content?: unknown; + stopReason?: string; usage?: { input_tokens?: number; output_tokens?: number; @@ -243,7 +246,8 @@ export function createAioCapture(args: { cacheCreationTokens: e.message.usage?.cache_creation_input_tokens, latencyMs, generationId: e.message.id, - isError: false, + isError: + e.message.stopReason === 'error' || e.message.stopReason === 'aborted', }; }; @@ -328,10 +332,7 @@ export function createAioCapture(args: { }); lastAssistantContent = turn.outputChoices; } - // Terminal: pi emits `agent_end` when the session's prompt() resolves. - if (e?.type === 'agent_end' && !e.willRetry) { - sendTraceEnd({ isError: false }); - } + // The harness decides whether a final turn recovered before ending the trace. } catch (err) { logToFile( `[aio-capture] pi compose failed: ${ @@ -340,5 +341,8 @@ export function createAioCapture(args: { ); } }, + finishPiRun(isError) { + sendTraceEnd({ isError }); + }, }; } diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index 79a87bfad..7279abc53 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -40,7 +40,7 @@ for the coordinated change checklist. Five layers, each with its own job. Nothing crosses layers unless it has to. **The entry point** (`index.ts`) is the front door: -`runAgent(config, input, {onProgress?, interaction?}) → RunResult`. It takes +`runAgent(config, input, {onProgress?, interaction?, signal?}) → RunResult`. It takes resolved execution data and an invocation snapshot (`shared/types.ts`), reports through `onProgress` and asks through `interaction` (`../progress.ts`), and returns every ending as a result. It never renders, reads a session or exits. @@ -86,6 +86,53 @@ gateway. Each layer is replaceable. +## Ownership map + +```mermaid +%%{init: {"block": {"padding": 20}}}%% +block-beta + columns 11 + hostBand["Host: programs and UI"]:11 + runProgramAgent["runProgramAgent"]:3 space:1 wizardAbort["wizardAbort"]:3 space:4 + space:11 + runnerBand["Agent runner"]:11 + runAgent["runAgent"]:3 space:1 runResult["RunResult"]:3 space:4 + space:11 + sequenceBand["Orchestrator sequence"]:11 + runOrchestrator["runOrchestrator"]:3 space:1 sequenceResult["SequenceResult"]:3 space:4 + space:11 + drainQueue["drainQueue"]:3 space:5 runAbort["AbortController"]:3 + space:11 + harnessBand["Selected harness"]:11 + agentHarness["AgentHarness"]:3 space:1 agentResult["AgentResult"]:3 space:1 signal["TaskRunInputs.signal"]:3 + space:11 + sdkBand["External model SDK"]:11 + sdk["Selected SDK"]:3 space:8 + + runProgramAgent --> runAgent + runAgent --> runOrchestrator + runOrchestrator --> drainQueue + drainQueue --> agentHarness + agentHarness --> sdk + agentHarness --> agentResult + agentResult --> sequenceResult + sequenceResult --> runResult + runResult --> wizardAbort + drainQueue --> runAbort + runAbort --> signal + + classDef owner fill:#9ca3af1f,stroke:#9ca3af,stroke-width:1.5px + classDef changed fill:#3b82f626,stroke:#3b82f6,stroke-width:2px + class hostBand,runnerBand,sequenceBand,harnessBand,sdkBand owner + class runResult,agentResult,runAbort changed +``` + +Calls descend on the left, results return through the middle, and cancellation +moves down the right. Blue marks the result contracts and run-scoped abort. +On the first fatal task result, `drainQueue` stops scheduling, cancels +active work and pending asks, joins siblings, then preserves that failure for +the host to present. + ## Flow 1. The caller runs its gates, authenticates, fetches PostHog flags and resolves diff --git a/src/agent/runner/harness/anthropic/index.ts b/src/agent/runner/harness/anthropic/index.ts index 74a051420..8a75169bf 100644 --- a/src/agent/runner/harness/anthropic/index.ts +++ b/src/agent/runner/harness/anthropic/index.ts @@ -75,7 +75,7 @@ export const anthropicBackend: AgentHarness = { logToFile('[agent-runner] agent initialized'); return executeAgent( - agent, + { ...agent, signal: inputs.signal }, prompt, runOptions(input), spinner, @@ -151,7 +151,7 @@ export const anthropicBackend: AgentHarness = { ); return executeAgent( - { ...agent, model, allowedTools, disallowedTools }, + { ...agent, model, allowedTools, disallowedTools, signal: inputs.signal }, prompt, options, spinner, diff --git a/src/agent/runner/harness/pi/__tests__/cancellation.test.ts b/src/agent/runner/harness/pi/__tests__/cancellation.test.ts new file mode 100644 index 000000000..c4b0d8607 --- /dev/null +++ b/src/agent/runner/harness/pi/__tests__/cancellation.test.ts @@ -0,0 +1,42 @@ +import { bindPiCancellation } from '../cancellation'; + +it('observes session abort and keeps listener failures out of abort dispatch', async () => { + const controller = new AbortController(); + let release: (() => void) | undefined; + const abort = vi.fn( + () => + new Promise((resolve) => { + release = resolve; + }), + ); + const binding = bindPiCancellation(controller.signal, { abort }); + expect(() => controller.abort()).not.toThrow(); + await Promise.resolve(); + expect(abort).toHaveBeenCalledOnce(); + let settled = false; + const waiting = binding.settle().then(() => { + settled = true; + }); + await Promise.resolve(); + expect(settled).toBe(false); + release?.(); + await waiting; + expect(settled).toBe(true); +}); + +it('contains a synchronous abort throw and a diagnostic callback throw', async () => { + const controller = new AbortController(); + const binding = bindPiCancellation( + controller.signal, + { + abort: () => { + throw new Error('abort failed'); + }, + }, + () => { + throw new Error('log failed'); + }, + ); + expect(() => controller.abort()).not.toThrow(); + await expect(binding.settle()).resolves.toBeUndefined(); +}); diff --git a/src/agent/runner/harness/pi/__tests__/gateway.test.ts b/src/agent/runner/harness/pi/__tests__/gateway.test.ts index cfedba108..548e77bb6 100644 --- a/src/agent/runner/harness/pi/__tests__/gateway.test.ts +++ b/src/agent/runner/harness/pi/__tests__/gateway.test.ts @@ -126,6 +126,15 @@ describe('isGatewayAuthRejection', () => { }), ).toBe(false); }); + + it('lets structured non-401 status override quoted 401 prose', () => { + expect( + isGatewayAuthRejection({ + errorMessage: 'Earlier log: API error 401', + diagnostics: [{ error: { code: 403 } }], + }), + ).toBe(false); + }); }); describe('withGatewayRemint', () => { @@ -180,6 +189,76 @@ describe('withGatewayRemint', () => { return { wrapped, registry, refreshAuth, prompts }; } + it.each([ + [ + { + stopReason: 'error', + errorMessage: 'bad request', + diagnostics: [{ error: { code: 400 } }], + }, + 'WIZARD_API_ERROR', + ], + [ + { + stopReason: 'error', + errorMessage: 'rate limited', + diagnostics: [{ error: { code: 429 } }], + }, + 'WIZARD_RATE_LIMIT', + ], + [ + { + stopReason: 'error', + errorMessage: 'service unavailable', + diagnostics: [{ error: { code: 503 } }], + }, + 'WIZARD_API_ERROR', + ], + [{ stopReason: 'aborted', errorMessage: 'cancelled' }, 'WIZARD_ABORT'], + ])('exposes a terminal provider turn %j', async (turn, classification) => { + const { wrapped } = harness(gatewayAuth('phe_fresh', Date.now() + HOUR), [ + turn, + ]); + await wrapped.prompt('do it'); + expect(wrapped.terminalFailure()).toMatchObject({ + classification, + message: turn.errorMessage, + }); + }); + + it('uses the final turn after remint and recovery', async () => { + const { wrapped } = harness(gatewayAuth('phe_old', Date.now() - 1), [ + rejected, + fine, + ]); + await wrapped.prompt('do it'); + expect(wrapped.terminalFailure()).toBeUndefined(); + }); + + it('uses rate-limit prose when diagnostics have no HTTP status', async () => { + const { wrapped } = harness(gatewayAuth('phe_fresh', Date.now() + HOUR), [ + { + stopReason: 'error', + errorMessage: '429 rate limit', + diagnostics: [{ error: { code: 'UNKNOWN' } }], + }, + ]); + await wrapped.prompt('do it'); + expect(wrapped.terminalFailure()?.classification).toBe('WIZARD_RATE_LIMIT'); + }); + + it('recognizes a name-only authentication error as an auth failure', async () => { + const { wrapped } = harness(gatewayAuth('phe_fresh', Date.now() + HOUR), [ + { + stopReason: 'error', + errorMessage: 'rejected', + diagnostics: [{ error: { name: 'AuthenticationError' } }], + }, + ]); + await wrapped.prompt('do it'); + expect(wrapped.terminalFailure()?.status).toBe(401); + }); + it('re-mints once and continues when a turn ends on a 401 from an aged bearer', async () => { const { wrapped, registry, refreshAuth, prompts } = harness( gatewayAuth('phe_old', Date.now() - 1), diff --git a/src/agent/runner/harness/pi/__tests__/mcp-env.test.ts b/src/agent/runner/harness/pi/__tests__/mcp-env.test.ts new file mode 100644 index 000000000..04c9a9c3d --- /dev/null +++ b/src/agent/runner/harness/pi/__tests__/mcp-env.test.ts @@ -0,0 +1,43 @@ +const state = vi.hoisted(() => ({ fail: false })); +vi.mock('jiti', () => ({ + createJiti: () => ({ + import: () => { + if (state.fail) return Promise.reject(new Error('adapter import failed')); + return Promise.resolve({ createMcpAdapter: () => () => undefined }); + }, + }), +})); +vi.mock('@utils/debug'); + +import { setupPostHogMcp } from '../mcp'; + +const envName = 'POSTHOG_MCP_TOKEN'; +const opts = (accessToken: string) => ({ + mcpUrl: 'https://mcp.test', + accessToken, + userAgent: 'test', +}); + +afterEach(() => { + delete process.env[envName]; + state.fail = false; +}); + +it('restores the prior token if adapter setup fails', async () => { + process.env[envName] = 'prior'; + state.fail = true; + await expect(setupPostHogMcp(opts('run-token'))).rejects.toThrow( + 'adapter import failed', + ); + expect(process.env[envName]).toBe('prior'); +}); + +it('keeps a shared token until the final owner releases it', async () => { + process.env[envName] = 'prior'; + const first = await setupPostHogMcp(opts('run-token')); + const second = await setupPostHogMcp(opts('run-token')); + first.cleanup(); + expect(process.env[envName]).toBe('run-token'); + second.cleanup(); + expect(process.env[envName]).toBe('prior'); +}); diff --git a/src/agent/runner/harness/pi/cancellation.ts b/src/agent/runner/harness/pi/cancellation.ts new file mode 100644 index 000000000..2f480f8b6 --- /dev/null +++ b/src/agent/runner/harness/pi/cancellation.ts @@ -0,0 +1,28 @@ +/** Bind a host signal to Pi's abort-and-wait-for-idle operation. */ +export function bindPiCancellation( + signal: AbortSignal | undefined, + session: { abort(): Promise }, + onAbortError?: (error: unknown) => void, +): { settle(): Promise } { + let abortPromise: Promise | undefined; + const onAbort = () => { + abortPromise ??= Promise.resolve() + .then(() => session.abort()) + .catch((error: unknown) => { + try { + onAbortError?.(error); + } catch { + /* Abort diagnostics are best effort. */ + } + }); + }; + signal?.addEventListener('abort', onAbort, { once: true }); + if (signal?.aborted) onAbort(); + + return { + async settle() { + signal?.removeEventListener('abort', onAbort); + await abortPromise; + }, + }; +} diff --git a/src/agent/runner/harness/pi/completion.ts b/src/agent/runner/harness/pi/completion.ts index 8219e5c46..facede847 100644 --- a/src/agent/runner/harness/pi/completion.ts +++ b/src/agent/runner/harness/pi/completion.ts @@ -21,7 +21,9 @@ export function completionFailure(args: { * each entry point decide the type first and hand it to the event, and keeps * the one rule in one place. */ -export function runErrorType(message: string): AgentErrorType { +export function runErrorType( + message: string, +): AgentErrorType.RATE_LIMIT | AgentErrorType.API_ERROR { const lower = message.toLowerCase(); if (lower.includes('rate limit') || lower.includes('429')) { return AgentErrorType.RATE_LIMIT; diff --git a/src/agent/runner/harness/pi/gateway.ts b/src/agent/runner/harness/pi/gateway.ts index 9c6a6d6c7..bb5b7e20a 100644 --- a/src/agent/runner/harness/pi/gateway.ts +++ b/src/agent/runner/harness/pi/gateway.ts @@ -15,6 +15,7 @@ import { modelCapabilities, type ThinkingLevel, } from '../../switchboard/models'; +import { AgentErrorType } from '@agent/signals'; /** Provider registered on the in-memory registry for this run. */ export const GATEWAY_PROVIDER = 'posthog-gateway'; @@ -146,6 +147,49 @@ export interface GatewayTurnError { diagnostics?: { error?: { name?: string; code?: string | number } }[]; } +export type GatewayTerminalFailure = { + classification: + | AgentErrorType.API_ERROR + | AgentErrorType.RATE_LIMIT + | AgentErrorType.ABORT; + message: string; + status?: number; +}; + +export function gatewayTerminalFailure( + turn: ({ stopReason?: string } & GatewayTurnError) | undefined, +): GatewayTerminalFailure | undefined { + if (!turn || (turn.stopReason !== 'error' && turn.stopReason !== 'aborted')) + return undefined; + const message = + turn.errorMessage || + (turn.stopReason === 'aborted' + ? 'Agent turn aborted' + : 'Gateway request failed'); + if (turn.stopReason === 'aborted') { + return { classification: AgentErrorType.ABORT, message }; + } + const codes = + turn.diagnostics + ?.map((d) => d.error?.code) + .filter((code) => code !== undefined) ?? []; + const structuredStatus = codes + .map(Number) + .find((code) => Number.isInteger(code) && code >= 400 && code < 600); + const rateLimited = + structuredStatus !== undefined + ? structuredStatus === 429 + : /\b429\b|rate limit/i.test(message); + return { + classification: rateLimited + ? AgentErrorType.RATE_LIMIT + : AgentErrorType.API_ERROR, + message, + status: + structuredStatus ?? (isGatewayAuthRejection(turn) ? 401 : undefined), + }; +} + /** * Whether a turn's error is the gateway rejecting the bearer. pi attaches the * SDK's own error to `diagnostics`, so its code decides when one is present. @@ -159,6 +203,10 @@ export function isGatewayAuthRejection( typeof turn === 'string' ? { errorMessage: turn, diagnostics: undefined } : turn ?? {}; + const structuredStatus = (diagnostics ?? []) + .map((diagnostic) => Number(diagnostic.error?.code)) + .find((code) => Number.isInteger(code) && code >= 400 && code < 600); + if (structuredStatus !== undefined) return structuredStatus === 401; for (const diagnostic of diagnostics ?? []) { const code = diagnostic.error?.code; if (code === 401 || code === '401') return true; @@ -169,6 +217,7 @@ export function isGatewayAuthRejection( } export interface GatewayRemintOptions { + signal?: AbortSignal; session: { prompt(text: string): Promise }; registry: { registerProvider(providerName: string, config: never): void }; auth: GatewayAuth; @@ -190,30 +239,40 @@ export function withGatewayRemint(opts: GatewayRemintOptions): { prompt(text: string): Promise; /** Feed every assistant `message_end`; the last turn decides. */ noteAssistantTurn(message: unknown): void; + terminalFailure(): GatewayTerminalFailure | undefined; } { let auth = opts.auth; let rejected = false; let reminted = false; + let lastTurn: ({ stopReason?: string } & GatewayTurnError) | undefined; return { noteAssistantTurn(message) { - const turn = message as + lastTurn = message as | ({ stopReason?: string } & GatewayTurnError) | undefined; - rejected = turn?.stopReason === 'error' && isGatewayAuthRejection(turn); + rejected = + lastTurn?.stopReason === 'error' && isGatewayAuthRejection(lastTurn); }, + terminalFailure: () => gatewayTerminalFailure(lastTurn), async prompt(text) { + if (opts.signal?.aborted) return; rejected = false; + lastTurn = undefined; await opts.session.prompt(text); - if (!rejected || reminted || !isPastRefresh(auth)) return; + if (opts.signal?.aborted || !rejected || reminted || !isPastRefresh(auth)) + return; reminted = true; auth = await opts.refreshAuth(); + if (opts.signal?.aborted) return; opts.registry.registerProvider( GATEWAY_PROVIDER, buildGatewayProvider(opts.providerInputs(auth)).provider as never, ); opts.onRemint?.(); rejected = false; + lastTurn = undefined; const next = opts.continueText; + if (opts.signal?.aborted) return; await opts.session.prompt(typeof next === 'function' ? next() : next); }, }; diff --git a/src/agent/runner/harness/pi/index.ts b/src/agent/runner/harness/pi/index.ts index ec903a4e9..90ea3ac54 100644 --- a/src/agent/runner/harness/pi/index.ts +++ b/src/agent/runner/harness/pi/index.ts @@ -44,6 +44,8 @@ import type { ProgressEmitter } from '@agent/progress'; import { createEmitLog } from '@agent/runner/shared/progress-collector'; import type { TaskStore } from './tasks'; import { completionFailure, runErrorType } from './completion'; +import { bindPiCancellation } from './cancellation'; +import { classifyRunFailure, ErrorCodes } from '@shared/errors'; /** Injects the MCP server `instructions` pi-mcp-adapter drops (project env, skill steer, tool domains) into the system prompt, falling back to a bootstrap-derived project block when the warm-connect captured none. */ function piMcpContext( @@ -205,6 +207,13 @@ export const piBackend: AgentHarness = { name: Harness.pi, async run(inputs: BackendRunInputs): Promise { + if (inputs.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } const { config: runConfig, input, boot, emit, prompt, spinner } = inputs; const config = runConfig.run; const modelId = inputs.model; @@ -255,6 +264,9 @@ export const piBackend: AgentHarness = { model: modelId, }); + let mcpCleanup: (() => void) | undefined; + let aioFailed = true; + let cancellation: ReturnType | undefined; try { const { createAgentSession, @@ -298,7 +310,8 @@ export const piBackend: AgentHarness = { const model = registry.find(GATEWAY_PROVIDER, modelId); if (!model) { return { - error: AgentErrorType.API_ERROR, + kind: 'failure', + classification: AgentErrorType.API_ERROR, message: 'pi: gateway model could not be resolved', }; } @@ -338,7 +351,6 @@ export const piBackend: AgentHarness = { const extensionFactories = [security.factory] as Array< (pi: unknown) => void >; - let mcpCleanup: (() => void) | undefined; let mcpInstructions: string | undefined; // Whether the agent really got the tool. The commandments below claim // `posthog_exec` exists when this is true, so it must track the setup and @@ -363,6 +375,12 @@ export const piBackend: AgentHarness = { mcpInstructions = await instructionsPromise; posthogMcp = true; } catch (err) { + try { + mcpCleanup?.(); + } catch { + /* Setup cleanup is best effort. */ + } + mcpCleanup = undefined; logToFile(`[pi] PostHog MCP setup skipped: ${String(err)}`); analytics.wizardCapture('mcp setup failed', { harness: 'pi', @@ -485,12 +503,20 @@ export const piBackend: AgentHarness = { // rebindCurrentSession. createAgentSession builds the session but does not // emit session_start on its own, and the MCP adapter connects on that // event; without this its tools report "MCP not initialized". + cancellation = bindPiCancellation( + inputs.signal, + agentSession, + (error) => { + logToFile(`[pi] abort failed: ${String(error)}`); + }, + ); await agentSession.bindExtensions({}); // A turn that ends on a 401 from an aged bearer re-mints once and // continues; pi resolves the provider's apiKey per request, so // re-registering is enough. const turns = withGatewayRemint({ + signal: inputs.signal, session: agentSession, registry, auth, @@ -573,10 +599,18 @@ export const piBackend: AgentHarness = { // turns that follow. capture.setInitialPrompt(prompt); + let terminal = turns.terminalFailure(); try { + if (inputs.signal?.aborted) + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; // Non-streaming: resolves when the agent run completes. Throws if no // model/api key, or on a transport error. await turns.prompt(prompt); + terminal = turns.terminalFailure(); // Completion guard: pi's prompt() resolves the moment the model returns // a turn with no tool call (e.g. a lone [STATUS] line), even mid-plan. @@ -585,6 +619,8 @@ export const piBackend: AgentHarness = { while ( continueNudges < MAX_CONTINUE_NUDGES && !security.state.criticalViolation && + !inputs.signal?.aborted && + !terminal && hasOpenTasks(wizardTaskTools.store) ) { continueNudges += 1; @@ -592,10 +628,15 @@ export const piBackend: AgentHarness = { `[pi] completion guard: tasks still open, nudge ${continueNudges}/${MAX_CONTINUE_NUDGES}`, ); await turns.prompt(CONTINUE_INSTRUCTION); + terminal = turns.terminalFailure(); } // Best-effort remark ask — a failed turn never fails a successful run. - if (!security.state.criticalViolation) { + if ( + !security.state.criticalViolation && + !terminal && + !inputs.signal?.aborted + ) { try { await agentSession.prompt(REMARK_INSTRUCTION); } catch (err) { @@ -603,8 +644,47 @@ export const piBackend: AgentHarness = { } } } finally { - unsubscribe(); - mcpCleanup?.(); + try { + unsubscribe(); + } catch { + /* Keep the terminal result. */ + } + } + + if (inputs.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } + + if (terminal && !security.state.criticalViolation) { + spinner.stop( + config.errorMessage ?? `${config.integrationLabel} failed`, + ); + captureAborted(terminal.classification); + if (terminal.status === 401) { + return { + kind: 'decided_failure', + failure: { + code: ErrorCodes.AuthInvalidOrExpired, + message: 'Authentication failed (401)', + detail: { providerMessage: terminal.message }, + }, + }; + } + return terminal.classification === AgentErrorType.ABORT + ? { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: terminal.message, + } + : { + kind: 'failure', + classification: terminal.classification, + message: terminal.message, + }; } // A latched post-scan violation terminates the run as a YARA violation, @@ -615,7 +695,10 @@ export const piBackend: AgentHarness = { `[pi] terminated: YARA violation (blocked ${security.state.blockedCount} call(s))`, ); captureAborted(AgentErrorType.YARA_VIOLATION); - return { error: AgentErrorType.YARA_VIOLATION }; + return { + kind: 'failure', + classification: AgentErrorType.YARA_VIOLATION, + }; } // pi ends a run on any tool-call-less turn, so guard against a hollow @@ -631,14 +714,14 @@ export const piBackend: AgentHarness = { assistant_turns: assistantTurns, }); captureAborted(failure); - return { error: failure }; + return { kind: 'failure', classification: failure }; } if (failure === AgentErrorType.INCOMPLETE_TASKS) { spinner.stop('Agent stopped before finishing'); logToFile('[pi] incomplete: tasks left open'); analytics.wizardCapture('agent incomplete tasks', { open_tasks: true }); captureAborted(failure); - return { error: failure }; + return { kind: 'failure', classification: failure }; } const remark = signals.remark(); @@ -678,15 +761,48 @@ export const piBackend: AgentHarness = { cache_read_input_tokens: stats.tokens.cacheRead, }); spinner.stop(config.successMessage ?? 'PostHog integration complete'); - return {}; + aioFailed = false; + return { kind: 'success' }; } catch (err) { + if (inputs.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } const message = err instanceof Error ? err.message : String(err); logToFile(`[pi] run error: ${message}`); spinner.stop(config.errorMessage ?? `${config.integrationLabel} failed`); log.error(`pi backend error: ${message}`); - const error = runErrorType(message); - captureAborted(error); - return { error, message }; + const coded = classifyRunFailure(err); + if (coded.coded && err instanceof Error) { + captureAborted(AgentErrorType.API_ERROR); + return { + kind: 'decided_failure', + failure: { code: coded.code, message: coded.message, error: err }, + }; + } + const classification = runErrorType(message); + captureAborted(classification); + return { + kind: 'failure', + classification, + message, + error: err instanceof Error ? err : undefined, + }; + } finally { + await cancellation?.settle(); + try { + mcpCleanup?.(); + } catch { + /* Keep the terminal result. */ + } + try { + capture.finishPiRun(aioFailed); + } catch { + /* Telemetry is best effort. */ + } } }, diff --git a/src/agent/runner/harness/pi/mcp.ts b/src/agent/runner/harness/pi/mcp.ts index 8f553a983..78ac2d684 100644 --- a/src/agent/runner/harness/pi/mcp.ts +++ b/src/agent/runner/harness/pi/mcp.ts @@ -11,11 +11,37 @@ import { VERSION } from '@shared/version'; import { logToFile } from '@utils/debug'; const MCP_TOKEN_ENV = 'POSTHOG_MCP_TOKEN'; +let mcpTokenOwners = 0; +let activeMcpToken: string | undefined; +let previousMcpToken: string | undefined; + +function acquireMcpToken(token: string): () => void { + if (mcpTokenOwners > 0 && activeMcpToken !== token) { + throw new Error('PostHog MCP token is already owned by another active run'); + } + if (mcpTokenOwners === 0) { + previousMcpToken = process.env[MCP_TOKEN_ENV]; + activeMcpToken = token; + process.env[MCP_TOKEN_ENV] = token; + } + mcpTokenOwners += 1; + let released = false; + return () => { + if (released) return; + released = true; + mcpTokenOwners -= 1; + if (mcpTokenOwners > 0) return; + if (previousMcpToken === undefined) delete process.env[MCP_TOKEN_ENV]; + else process.env[MCP_TOKEN_ENV] = previousMcpToken; + activeMcpToken = undefined; + previousMcpToken = undefined; + }; +} export interface PostHogMcpSetup { /** pi ExtensionFactory to add to the resource loader's `extensionFactories`. */ extensionFactory: (pi: unknown) => void; - /** Drop the token env var. Call after the run. */ + /** Release this setup's token ownership. Call after the run. */ cleanup: () => void; } @@ -55,37 +81,39 @@ export async function setupPostHogMcp(opts: { const { mcpUrl, accessToken, userAgent } = opts; // By env NAME: the token stays in this process, off disk, and never reaches pi's env-scrubbed tool subprocesses. - process.env[MCP_TOKEN_ENV] = accessToken; + const cleanup = acquireMcpToken(accessToken); - // The adapter ships raw TypeScript; loading through jiti is its documented requirement. - const jiti = createJiti(import.meta.url); - const mod = await jiti.import<{ - createMcpAdapter: (options: unknown) => PostHogMcpSetup['extensionFactory']; - }>('pi-mcp-adapter'); - const extensionFactory = mod.createMcpAdapter({ - config: { - mcpServers: { - posthog: { - url: mcpUrl, - auth: 'bearer', - bearerTokenEnv: MCP_TOKEN_ENV, - headers: { 'User-Agent': userAgent }, - // Connect at extension load — direct tools register without a session_start. - lifecycle: 'eager', - // Register only `exec`: `directTools: true` also mints a `posthog_get_` tool per MCP resource, whose sentence-length names overflow Anthropic's 128-char tool-name limit and 400 the whole request. - directTools: ['exec'], - exposeResources: false, + try { + // The adapter ships raw TypeScript; loading through jiti is its documented requirement. + const jiti = createJiti(import.meta.url); + const mod = await jiti.import<{ + createMcpAdapter: ( + options: unknown, + ) => PostHogMcpSetup['extensionFactory']; + }>('pi-mcp-adapter'); + const extensionFactory = mod.createMcpAdapter({ + config: { + mcpServers: { + posthog: { + url: mcpUrl, + auth: 'bearer', + bearerTokenEnv: MCP_TOKEN_ENV, + headers: { 'User-Agent': userAgent }, + // Connect at extension load — direct tools register without a session_start. + lifecycle: 'eager', + // Register only `exec`: `directTools: true` also mints a `posthog_get_` tool per MCP resource, whose sentence-length names overflow Anthropic's 128-char tool-name limit and 400 the whole request. + directTools: ['exec'], + exposeResources: false, + }, }, + // Disable the proxy `mcp` tool (its search indirection pollutes context); the adapter re-enables it only if no direct tools resolve. + settings: { disableProxyTool: true, toolPrefix: 'posthog' }, }, - // Disable the proxy `mcp` tool (its search indirection pollutes context); the adapter re-enables it only if no direct tools resolve. - settings: { disableProxyTool: true, toolPrefix: 'posthog' }, - }, - }); - logToFile(`[pi-mcp] adapter loaded; posthog MCP at ${mcpUrl}`); - - const cleanup = (): void => { - delete process.env[MCP_TOKEN_ENV]; - }; - - return { extensionFactory, cleanup }; + }); + logToFile(`[pi-mcp] adapter loaded; posthog MCP at ${mcpUrl}`); + return { extensionFactory, cleanup }; + } catch (error) { + cleanup(); + throw error; + } } diff --git a/src/agent/runner/harness/pi/subagent.ts b/src/agent/runner/harness/pi/subagent.ts index 1f5e7f7d6..1238ca262 100644 --- a/src/agent/runner/harness/pi/subagent.ts +++ b/src/agent/runner/harness/pi/subagent.ts @@ -17,6 +17,7 @@ import { Type } from 'typebox'; import { defineTool } from '@earendil-works/pi-coding-agent'; import type { ToolDefinition } from '@earendil-works/pi-coding-agent'; import { logToFile } from '@utils/debug'; +import { gatewayTerminalFailure } from './gateway'; /** * Read-only built-ins a subagent may use. bash is supplied separately as the @@ -115,8 +116,19 @@ export function createDispatchAgentTool(ctx: SubagentContext): ToolDefinition { }); let result = ''; + let finalTurn: + | { + stopReason?: string; + errorMessage?: string; + diagnostics?: { + error?: { name?: string; code?: string | number }; + }[]; + } + | undefined; const unsub = child.subscribe((e) => { if (e.type === 'message_end') { + if ((e.message as { role?: string })?.role === 'assistant') + finalTurn = e.message as typeof finalTurn; const t = extractText(e.message).trim(); if (t) result = t; } @@ -124,6 +136,8 @@ export function createDispatchAgentTool(ctx: SubagentContext): ToolDefinition { logToFile(`[pi] subagent dispatch: ${args.description}`); try { await child.prompt(args.prompt); + const failure = gatewayTerminalFailure(finalTurn); + if (failure) throw new Error(`Subagent failed: ${failure.message}`); } finally { unsub(); } diff --git a/src/agent/runner/harness/pi/task.ts b/src/agent/runner/harness/pi/task.ts index 76db7a43a..290950f40 100644 --- a/src/agent/runner/harness/pi/task.ts +++ b/src/agent/runner/harness/pi/task.ts @@ -42,6 +42,8 @@ import { withGatewayRemint, } from './gateway'; import { runErrorType } from './completion'; +import { bindPiCancellation } from './cancellation'; +import { classifyRunFailure, ErrorCodes } from '@shared/errors'; import { assembleCommandments } from '../../switchboard/commandments'; import { applyOutroMarkers, @@ -164,6 +166,13 @@ function isSettled(ctx: OrchestratorToolsContext): boolean { } export async function runPiTask(inputs: TaskRunInputs): Promise { + if (inputs.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } const { config, input, @@ -220,6 +229,9 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { ...analyticsProperties, }); + let mcpCleanup: (() => void) | undefined; + let aioFailed = true; + let cancellation: ReturnType | undefined; try { const sdk = await import('@earendil-works/pi-coding-agent'); const { @@ -262,7 +274,8 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { const model = registry.find(GATEWAY_PROVIDER, modelId); if (!model) { return { - error: AgentErrorType.API_ERROR, + kind: 'failure', + classification: AgentErrorType.API_ERROR, message: 'pi: gateway model could not be resolved', }; } @@ -290,7 +303,6 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { const extensionFactories = [security.factory] as Array< (pi: unknown) => void >; - let mcpCleanup: (() => void) | undefined; let posthogMcp = false; if (allowsPostHogMcp(allowedTools)) { try { @@ -304,6 +316,12 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { mcpCleanup = mcp.cleanup; posthogMcp = true; } catch (err) { + try { + mcpCleanup?.(); + } catch { + /* Setup cleanup is best effort. */ + } + mcpCleanup = undefined; // Silent here reads as a task failure minutes later: a task that asked // for this tool can only skip or fail without it. logToFile(`[pi-task] PostHog MCP setup skipped: ${String(err)}`); @@ -394,11 +412,15 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { noTools: 'builtin', customTools, }); + cancellation = bindPiCancellation(inputs.signal, agentSession, (error) => { + logToFile(`[pi-task] abort failed: ${String(error)}`); + }); await agentSession.bindExtensions({}); // A turn that ends on a 401 from an aged bearer re-mints once and // continues with the nudge the task would get anyway. const turns = withGatewayRemint({ + signal: inputs.signal, session: agentSession, registry, auth, @@ -472,8 +494,16 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { // before it reaches this call site). capture.setInitialPrompt(taskPrompt); + let terminal = turns.terminalFailure(); try { + if (inputs.signal?.aborted) + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; await turns.prompt(taskPrompt); + terminal = turns.terminalFailure(); // pi's prompt() resolves the moment a turn carries no tool call — which // an agent mid-plan does emit. While the work has not reached its @@ -482,6 +512,8 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { while ( nudges < MAX_TASK_NUDGES && !security.state.criticalViolation && + !inputs.signal?.aborted && + !terminal && !isSettled(orchestrator) ) { nudges += 1; @@ -491,9 +523,15 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { await turns.prompt( orchestrator.currentTaskId ? TASK_NUDGE : SEED_NUDGE, ); + terminal = turns.terminalFailure(); } - if (requestRemark && !security.state.criticalViolation) { + if ( + requestRemark && + !security.state.criticalViolation && + !terminal && + !inputs.signal?.aborted + ) { try { await agentSession.prompt(REMARK_INSTRUCTION); } catch (err) { @@ -501,8 +539,46 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { } } } finally { - unsubscribe(); - mcpCleanup?.(); + try { + unsubscribe(); + } catch { + /* Keep the terminal result. */ + } + } + + if (inputs.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } + + if (terminal && !security.state.criticalViolation) { + if (errorMessage || spinnerMessage) + spinner.stop(errorMessage ?? 'Task failed'); + captureAborted(terminal.classification); + if (terminal.status === 401) { + return { + kind: 'decided_failure', + failure: { + code: ErrorCodes.AuthInvalidOrExpired, + message: 'Authentication failed (401)', + detail: { providerMessage: terminal.message }, + }, + }; + } + return terminal.classification === AgentErrorType.ABORT + ? { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: terminal.message, + } + : { + kind: 'failure', + classification: terminal.classification, + message: terminal.message, + }; } if (security.state.criticalViolation) { @@ -511,7 +587,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { `[pi-task] terminated: YARA violation (blocked ${security.state.blockedCount} call(s))`, ); captureAborted(AgentErrorType.YARA_VIOLATION); - return { error: AgentErrorType.YARA_VIOLATION }; + return { kind: 'failure', classification: AgentErrorType.YARA_VIOLATION }; } const remark = signals.remark(); @@ -542,15 +618,48 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { `[pi-task] usage task=${taskType} model=${modelId} effort=${caps.thinkingLevel} dur=${durations.duration_seconds}s turns=${assistantTurns} in=${stats.tokens.input} out=${stats.tokens.output} cacheR=${stats.tokens.cacheRead} cacheW=${stats.tokens.cacheWrite}`, ); if (successMessage) spinner.stop(successMessage); - return {}; + aioFailed = false; + return { kind: 'success' }; } catch (err) { + if (inputs.signal?.aborted) { + return { + kind: 'abort', + classification: AgentErrorType.ABORT, + message: 'Agent run cancelled', + }; + } const message = err instanceof Error ? err.message : String(err); logToFile(`[pi-task] run error: ${message}`); if (errorMessage || spinnerMessage) { spinner.stop(errorMessage ?? 'Task failed'); } - const error = runErrorType(message); - captureAborted(error); - return { error, message }; + const coded = classifyRunFailure(err); + if (coded.coded && err instanceof Error) { + captureAborted(AgentErrorType.API_ERROR); + return { + kind: 'decided_failure', + failure: { code: coded.code, message: coded.message, error: err }, + }; + } + const classification = runErrorType(message); + captureAborted(classification); + return { + kind: 'failure', + classification, + message, + error: err instanceof Error ? err : undefined, + }; + } finally { + await cancellation?.settle(); + try { + mcpCleanup?.(); + } catch { + /* Keep the terminal result. */ + } + try { + capture.finishPiRun(aioFailed); + } catch { + /* Telemetry is best effort. */ + } } } diff --git a/src/agent/runner/harness/types.ts b/src/agent/runner/harness/types.ts index 80d4786c8..35da58793 100644 --- a/src/agent/runner/harness/types.ts +++ b/src/agent/runner/harness/types.ts @@ -15,8 +15,7 @@ * rather than silently downgrading. * * A harness reports through `emit` and never reaches for a UI. It returns an - * error classification, or a decided `failure` the sequence must return as - * the run's result. + * tagged success, abort, or failure data for the sequence to decide. */ import type { AdditionalFeature } from '@shared/constants'; @@ -52,6 +51,7 @@ export interface BackendRunInputs { input: RunInput; boot: BootstrapResult; emit: ProgressEmitter; + signal?: AbortSignal; /** The fully assembled prompt. */ prompt: string; /** Installed framework-skill path, when the program installs one. */ @@ -69,15 +69,24 @@ export interface BackendRunInputs { } /** - * What a runner reports back: an error classification, or nothing on success. - * `failure` is a fully decided abort the harness already reported to the host - * (the 401 auth screen); the sequence returns it as the run's result. + * A harness reports one terminal outcome. A decided failure already has the + * caller-visible code and message; the caller alone presents it. */ -export type AgentResult = { - error?: AgentErrorType; - message?: string; - failure?: AgentFailure; -}; +export type AgentResult = + | { kind: 'success' } + | { + kind: 'abort'; + classification: AgentErrorType.ABORT; + message?: string; + error?: Error; + } + | { + kind: 'failure'; + classification: Exclude; + message?: string; + error?: Error; + } + | { kind: 'decided_failure'; failure: AgentFailure }; /** * One orchestrator-mode unit of work — the seed plan, or one drained task. @@ -91,6 +100,7 @@ export interface TaskRunInputs { input: RunInput; boot: BootstrapResult; emit: ProgressEmitter; + signal?: AbortSignal; /** The fully assembled per-task or seed prompt. */ prompt: string; spinner: SpinnerHandle; diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index 9f583e16c..f6b83d31c 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -25,7 +25,7 @@ import { Sequence } from '@shared/constants'; import { RunOutcome } from './shared/types'; export { RunOutcome } from './shared/types'; -import { classifyRunFailure } from '@shared/errors'; +import { classifyRunFailure, ErrorCodes } from '@shared/errors'; import { logToFile } from '@utils/debug'; import type { RunAgentOptions, @@ -76,54 +76,152 @@ export async function runAgent( input: RunInput, options: RunAgentOptions = {}, ): Promise { - const collector = createProgressCollector(options.onProgress); - const { emit } = collector; - const log = (message: string) => - emit({ kind: 'log', level: 'info', message }); - - // Flush the warlock scan report once, at this single seam, on every - // termination path and for every harness (linear, orchestrator, or future). - // flushScanReport is idempotent (it zeroes scan state), so a caller that also - // flushes from its own cleanup path sees a harmless no-op. No harness has to - // know reporting exists. + let collector: ReturnType | undefined; + const snapshot = (): RunResult['snapshot'] => { + try { + if (collector) return collector.snapshot(); + } catch { + // A partial snapshot must not replace the run's primary failure. + } + return { + tasks: [], + statusMessages: [], + usage: { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + }, + }; + }; + let result: RunResult; try { + collector = createProgressCollector(options.onProgress); + const { emit } = collector; + const log = (message: string) => + emit({ kind: 'log', level: 'info', message }); + if (options.signal?.aborted) { + try { + flushScanReport({ yaraReport: input.flags.yaraReport }); + } catch { + // Scan reporting is best effort. + } + return { + outcome: RunOutcome.Aborted, + skillId: input.skillId, + failure: { + code: ErrorCodes.AgentAbort, + message: 'Agent run cancelled', + }, + snapshot: snapshot(), + }; + } const boot = await prepareRun(config, input); if (config.binding.sequence === Sequence.orchestrator) { log('Task-queue orchestrator enabled.'); } - logToFile( - `[agent-runner] run program=${config.programId} sequence=${config.binding.sequence}` + - ` harness=${config.binding.harness} composed=${config.composed}`, - ); - const result = await getSequence(config.binding.sequence).run({ + try { + logToFile( + `[agent-runner] run program=${config.programId} sequence=${config.binding.sequence}` + + ` harness=${config.binding.harness} composed=${config.composed}`, + ); + } catch { + // Logging is best effort. + } + const sequenceResult = await getSequence(config.binding.sequence).run({ config, input, boot, emit, interaction: options.interaction, + signal: options.signal, }); - return { - ...result, + result = { + ...(options.signal?.aborted && + sequenceResult.outcome === RunOutcome.Success + ? { + outcome: RunOutcome.Aborted as const, + failure: { + code: ErrorCodes.AgentAbort, + message: 'Agent run cancelled', + }, + } + : sequenceResult), skillId: input.skillId, - snapshot: collector.snapshot(), + snapshot: snapshot(), }; } catch (error) { - // Not a decision the agent made. Hand it back whole rather than throw, so - // every ending of a run is a result the caller reads the same way. - const failure = classifyRunFailure(error); - logToFile('[agent-runner] run crashed:', error); - return { - outcome: RunOutcome.Crashed, - skillId: input.skillId, - failure: { - code: failure.code, - message: failure.message, - error: error instanceof Error ? error : new Error(String(error)), - }, - snapshot: collector.snapshot(), - }; - } finally { + const original = + error instanceof Error ? error : new Error(safeErrorMessage(error)); + let failure: ReturnType; + try { + failure = classifyRunFailure(original); + } catch { + failure = { + code: ErrorCodes.InternalUnhandled, + message: 'Unexpected agent error', + coded: false, + }; + } + try { + logToFile('[agent-runner] run failed:', original); + } catch { + // Logging is best effort. + } + let abortError = false; + try { + abortError = original.name === 'AbortError'; + } catch { + /* Hostile Error getter. */ + } + if (failure.coded) { + result = { + outcome: RunOutcome.Failed, + skillId: input?.skillId, + failure: { + code: failure.code, + message: failure.message, + error: original, + }, + snapshot: snapshot(), + }; + } else if (options.signal?.aborted && abortError) { + result = { + outcome: RunOutcome.Aborted, + skillId: input?.skillId, + failure: { + code: ErrorCodes.AgentAbort, + message: 'Agent run cancelled', + }, + snapshot: snapshot(), + }; + } else { + result = { + outcome: RunOutcome.Crashed, + skillId: input?.skillId, + failure: { + code: failure.code, + message: failure.message, + error: original, + }, + snapshot: snapshot(), + }; + } + } + try { const report = flushScanReport({ yaraReport: input.flags.yaraReport }); - if (report) log(report); + if (report) + collector?.emit({ kind: 'log', level: 'info', message: report }); + } catch { + // Scan reporting is best effort after the run outcome is decided. + } + return result; +} + +function safeErrorMessage(error: unknown): string { + try { + return String(error); + } catch { + return 'Unknown thrown value'; } } diff --git a/src/agent/runner/sequence/linear.ts b/src/agent/runner/sequence/linear.ts index e86ccbf4d..bbb58955f 100644 --- a/src/agent/runner/sequence/linear.ts +++ b/src/agent/runner/sequence/linear.ts @@ -11,10 +11,10 @@ */ import { OutroKind, type OutroData } from '@agent/progress'; -import { AgentErrorType, AgentSignals } from '../../agent-interface'; +import { AgentErrorType } from '../../agent-interface'; import { logToFile } from '@utils/debug'; import { createBenchmarkPipeline } from '@agent/middleware/benchmark'; -import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@shared/errors'; +import { AGENT_ERROR_CODE, ErrorCodes } from '@shared/errors'; import { analytics } from '@utils/analytics'; import { formatYaraAbortMessage } from '@agent/yara-hooks'; import { installSkillById } from '@agent/tools'; @@ -33,10 +33,16 @@ export async function runLinearProgram({ boot, emit, interaction, + signal, }: SequenceContext): Promise { const { run, composed } = config; const { skillsBaseUrl, credentials, project } = boot; const { projectApiKey, host, projectId } = credentials; + const aborted = (): SequenceResult => ({ + outcome: RunOutcome.Aborted, + failure: { code: ErrorCodes.AgentAbort, message: 'Agent run cancelled' }, + }); + if (signal?.aborted) return aborted(); // 5. Skill install (if skillId provided) let skillPath: string | undefined; @@ -48,6 +54,7 @@ export async function runLinearProgram({ skillsBaseUrl, { triage: boot.triageProvider }, ); + if (signal?.aborted) return aborted(); if (installResult.kind !== 'ok') { return failed(installFailure(run.integrationLabel, installResult)); } @@ -72,6 +79,7 @@ export async function runLinearProgram({ getSource: () => input.skillId ?? run.integrationLabel, richLinks: run.richLinks ?? false, timeoutMs: run.askTimeoutMs, + signal, }); const middleware = input.flags.benchmark @@ -95,6 +103,7 @@ export async function runLinearProgram({ : null, }); logToFile(`[agent-runner] prompt assembled (${prompt.length} chars)`); + if (signal?.aborted) return aborted(); // 8. Run the agent through the run-level harness. The harness owns the agent // loop + model transport; everything around it (skill install, prompt, ask @@ -112,14 +121,23 @@ export async function runLinearProgram({ middleware, model, thinkingLevel, + signal, }); + if (signal?.aborted && agentResult.kind === 'success') return aborted(); // 9. Error handling (full set from both harnesses) - if (agentResult.failure) { + if (agentResult.kind === 'decided_failure') { return failed(agentResult.failure); } - if (agentResult.error === AgentErrorType.ABORT) { + if (agentResult.kind === 'success') { + // Success continues through the post-run hooks and outro below. + } else if (agentResult.kind !== 'abort' && agentResult.kind !== 'failure') { + const _exhaustive: never = agentResult; + return _exhaustive; + } + + if (agentResult.kind === 'abort') { const reason = agentResult.message ?? ''; const matched = run.abortCases?.find((c) => c.match.test(reason)); const abortCode = matched?.errorCode ?? ErrorCodes.AgentAbort; @@ -148,22 +166,20 @@ export async function runLinearProgram({ return { outcome: RunOutcome.Aborted, failure: { + message: matched?.message ?? `${run.integrationLabel} aborted`, outroData, code: abortCode, - error: new WizardError( - `Agent aborted: ${reason}`, - { - integration: run.integrationLabel, - error_type: AgentErrorType.ABORT, - reason, - }, - abortCode, - ), + error: agentResult.error, }, }; } - if (agentResult.error === AgentErrorType.MCP_MISSING) { + const classification = + agentResult.kind === 'failure' ? agentResult.classification : undefined; + const failureMessage = + agentResult.kind === 'failure' ? agentResult.message : undefined; + + if (classification === AgentErrorType.MCP_MISSING) { return failed({ code: AGENT_ERROR_CODE[AgentErrorType.MCP_MISSING], message: @@ -171,53 +187,30 @@ export async function runLinearProgram({ 'The wizard was unable to connect to the PostHog MCP server.\n' + 'This could be due to a network issue or a configuration problem.\n\n' + `Please try again, or check the documentation:\n${run.docsUrl}`, - error: new WizardError( - 'Agent could not access PostHog MCP server', - { - integration: run.integrationLabel, - error_type: AgentErrorType.MCP_MISSING, - signal: AgentSignals.ERROR_MCP_MISSING, - }, - AGENT_ERROR_CODE[AgentErrorType.MCP_MISSING], - ), + error: agentResult.kind === 'failure' ? agentResult.error : undefined, }); } - if (agentResult.error === AgentErrorType.RESOURCE_MISSING) { + if (classification === AgentErrorType.RESOURCE_MISSING) { return failed({ code: AGENT_ERROR_CODE[AgentErrorType.RESOURCE_MISSING], message: 'Could not access the setup resource\n\n' + 'This may indicate a version mismatch or a temporary service issue.\n\n' + `Please try again, or check the documentation:\n${run.docsUrl}`, - error: new WizardError( - 'Agent could not access setup resource', - { - integration: run.integrationLabel, - error_type: AgentErrorType.RESOURCE_MISSING, - signal: AgentSignals.ERROR_RESOURCE_MISSING, - }, - AGENT_ERROR_CODE[AgentErrorType.RESOURCE_MISSING], - ), + error: agentResult.kind === 'failure' ? agentResult.error : undefined, }); } - if (agentResult.error === AgentErrorType.YARA_VIOLATION) { + if (classification === AgentErrorType.YARA_VIOLATION) { return failed({ code: AGENT_ERROR_CODE[AgentErrorType.YARA_VIOLATION], message: formatYaraAbortMessage(), - error: new WizardError( - 'YARA scanner terminated session', - { - integration: run.integrationLabel, - error_type: AgentErrorType.YARA_VIOLATION, - }, - AGENT_ERROR_CODE[AgentErrorType.YARA_VIOLATION], - ), + error: agentResult.kind === 'failure' ? agentResult.error : undefined, }); } - if (agentResult.error === AgentErrorType.NO_PROGRESS) { + if (classification === AgentErrorType.NO_PROGRESS) { analytics.wizardCapture('agent no progress', { integration: run.integrationLabel, error_type: AgentErrorType.NO_PROGRESS, @@ -227,18 +220,11 @@ export async function runLinearProgram({ message: 'The Wizard exited without changing your project. Please contact the ' + 'PostHog team with wizard@posthog.com about this error.', - error: new WizardError( - 'Agent made no progress', - { - integration: run.integrationLabel, - error_type: AgentErrorType.NO_PROGRESS, - }, - AGENT_ERROR_CODE[AgentErrorType.NO_PROGRESS], - ), + error: agentResult.kind === 'failure' ? agentResult.error : undefined, }); } - if (agentResult.error === AgentErrorType.INCOMPLETE_TASKS) { + if (classification === AgentErrorType.INCOMPLETE_TASKS) { analytics.wizardCapture('agent incomplete tasks', { integration: run.integrationLabel, error_type: AgentErrorType.INCOMPLETE_TASKS, @@ -248,46 +234,41 @@ export async function runLinearProgram({ message: 'The Wizard exited without completing its planned tasks. Please contact ' + 'the PostHog team with wizard@posthog.com about this error.', - error: new WizardError( - 'Agent left planned tasks incomplete', - { - integration: run.integrationLabel, - error_type: AgentErrorType.INCOMPLETE_TASKS, - }, - AGENT_ERROR_CODE[AgentErrorType.INCOMPLETE_TASKS], - ), + error: agentResult.kind === 'failure' ? agentResult.error : undefined, }); } if ( - agentResult.error === AgentErrorType.RATE_LIMIT || - agentResult.error === AgentErrorType.API_ERROR + classification === AgentErrorType.RATE_LIMIT || + classification === AgentErrorType.API_ERROR ) { analytics.wizardCapture('agent api error', { integration: run.integrationLabel, - error_type: agentResult.error, - error_message: agentResult.message, + error_type: classification, + error_message: failureMessage, }); return failed({ - code: AGENT_ERROR_CODE[agentResult.error], + code: AGENT_ERROR_CODE[classification], message: `API Error\n\n${ - agentResult.message || 'Unknown error' + failureMessage || 'Unknown error' }\n\nPlease report this to: wizard@posthog.com`, - error: new WizardError( - `API error: ${agentResult.message}`, - { - integration: run.integrationLabel, - error_type: agentResult.error, - }, - AGENT_ERROR_CODE[agentResult.error], - ), + error: agentResult.kind === 'failure' ? agentResult.error : undefined, + }); + } + + if (agentResult.kind === 'failure') { + return failed({ + code: AGENT_ERROR_CODE[agentResult.classification], + message: agentResult.message ?? 'Agent failed', + error: agentResult.error, }); } // 10. Post-run hooks if (config.hooks?.postRun) { await config.hooks.postRun(credentials); + if (signal?.aborted) return aborted(); } // A composed sub-run leaves the terminal outro to its host. diff --git a/src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts index a5236f1f5..aa4e9647a 100644 --- a/src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/executor.test.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { ErrorCodes } from '@shared/errors'; import { QueueStore, TaskStatus, @@ -41,7 +42,10 @@ describe('drainQueue', () => { }; it('waits for live siblings after a fatal error and starts no dependents', async () => { - const fatal = new RunTaskFatal({ message: 'Authentication failed' }); + const fatal = new RunTaskFatal({ + code: ErrorCodes.AgentOrchestratorTasksFailed, + message: 'Authentication failed', + }); let release!: () => void; const blocked = new Promise((resolve) => { release = resolve; @@ -73,7 +77,10 @@ describe('drainQueue', () => { it('preserves a fatal failure when a sibling completes in the same turn', async () => { q.enqueue({ type: 'success' }); q.enqueue({ type: 'fatal' }); - const fatal = new RunTaskFatal({ message: 'Authentication failed' }); + const fatal = new RunTaskFatal({ + code: ErrorCodes.AgentOrchestratorTasksFailed, + message: 'Authentication failed', + }); await expect( drainQueue(q, (task) => { if (task.type === 'fatal') return Promise.reject(fatal); @@ -83,6 +90,51 @@ describe('drainQueue', () => { ).rejects.toBe(fatal); }); + it('cancels and joins an active sibling while retaining the first fatal', async () => { + const controller = new AbortController(); + const fatal = new RunTaskFatal({ + code: ErrorCodes.AgentOrchestratorTasksFailed, + message: 'First failure', + }); + const later = new RunTaskFatal({ + code: ErrorCodes.AgentOrchestratorTasksFailed, + message: 'Abort noise', + }); + const a = q.enqueue({ type: 'fatal' }); + q.enqueue({ type: 'asking' }); + const queued = q.enqueue({ type: 'queued', dependsOn: [a.id] }); + const started: string[] = []; + let siblingSettled = false; + const result = drainQueue( + q, + async (task) => { + started.push(task.type); + if (task.type === 'fatal') throw fatal; + await new Promise((resolve) => + controller.signal.addEventListener('abort', () => resolve(), { + once: true, + }), + ); + siblingSettled = true; + throw later; + }, + { + maxStarts: 50, + signal: controller.signal, + onFatal: () => { + controller.abort(); + throw new Error('Cancellation cleanup failed'); + }, + }, + ).catch((error: unknown) => error); + + expect(await result).toBe(fatal); + expect(controller.signal.aborted).toBe(true); + expect(siblingSettled).toBe(true); + expect(started).toEqual(['fatal', 'asking']); + expect(q.get(queued.id)?.status).toBe(TaskStatus.Pending); + }); + it('runs a single task to done and drains', async () => { const a = q.enqueue({ type: 'install' }); await drainQueue(q, completing, { maxStarts: 50 }); diff --git a/src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts b/src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts index f1227f562..f46dc863d 100644 --- a/src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts +++ b/src/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts @@ -81,6 +81,18 @@ describe('task notice timeout', () => { } }); + it('closes an active notice on run cancellation without marking a timeout', async () => { + const controller = new AbortController(); + showTaskNotice.mockReturnValue(new Promise(() => undefined)); + const result = offerSeededTask(NOTICE, { + interaction, + signal: controller.signal, + }); + controller.abort(); + await expect(result).resolves.toEqual({ keep: false, timedOut: false }); + expect(cancelTaskNotice).toHaveBeenCalledTimes(1); + }); + it('keeps the step when the user accepts in time', async () => { vi.useFakeTimers(); try { diff --git a/src/agent/runner/sequence/orchestrator/executor.ts b/src/agent/runner/sequence/orchestrator/executor.ts index e70e5723c..f33baebdc 100644 --- a/src/agent/runner/sequence/orchestrator/executor.ts +++ b/src/agent/runner/sequence/orchestrator/executor.ts @@ -10,7 +10,7 @@ * injected: the real one spins up a fresh agent, the tests use a fake. */ import { analytics } from '@utils/analytics'; -import type { AgentFailure } from '../../shared/types'; +import { RunOutcome, type AgentFailure } from '../../shared/types'; import { logToFile } from '@utils/debug'; import { TaskStatus, type QueueStore, type QueuedTask } from './queue'; @@ -41,7 +41,12 @@ export type RunTask = (task: QueuedTask) => Promise; * the harness used to exit the process. */ export class RunTaskFatal extends Error { - constructor(public readonly failure: AgentFailure) { + constructor( + public readonly failure: AgentFailure, + public readonly outcome: + | RunOutcome.Aborted + | RunOutcome.Failed = RunOutcome.Failed, + ) { super(failure.message ?? 'agent run failed'); this.name = 'RunTaskFatal'; } @@ -50,6 +55,8 @@ export class RunTaskFatal extends Error { export interface DrainOptions { /** Backstop against a pathological always-one-more-pending loop. */ maxStarts: number; + signal?: AbortSignal; + onFatal?: () => void; } export const DEFAULT_DRAIN_OPTIONS: DrainOptions = { @@ -60,11 +67,14 @@ async function runOne( store: QueueStore, runTask: RunTask, task: QueuedTask, + signal?: AbortSignal, ): Promise { + if (signal?.aborted) return; store.start(task.id); try { await runTask(task); } catch (error) { + if (signal?.aborted) return; if (error instanceof RunTaskFatal) throw error; // The task threw rather than reporting. The outcome check below handles // the queue; the exception itself should never be silent. @@ -75,6 +85,8 @@ async function runOne( ); } + if (signal?.aborted) return; + const after = store.get(task.id); if (!after) return; @@ -116,12 +128,28 @@ export async function drainQueue( try { for (;;) { + if (opts.signal?.aborted) break; if (failure) throw failure.error; for (const task of store.nextRunnable()) { + if (opts.signal?.aborted) break; if (++starts > opts.maxStarts) break; - const p = runOne(store, runTask, task) + const p = runOne(store, runTask, task, opts.signal) .catch((error: unknown) => { - failure ??= { error }; + if (!failure) { + failure = { error }; + try { + opts.onFatal?.(); + } catch (abortError) { + try { + logToFile( + '[executor] fatal cancellation failed:', + abortError, + ); + } catch { + // Reporting cancellation failure cannot replace the fatal. + } + } + } }) .finally(() => running.delete(task.id)); running.set(task.id, p); @@ -133,4 +161,5 @@ export async function drainQueue( // No queue or skill cleanup may run while a sibling still uses them. await Promise.allSettled(running.values()); } + if (failure) throw failure.error; } diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 2d28b1d61..fbb024137 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -30,7 +30,13 @@ import { analytics } from '@utils/analytics'; import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; -import { ErrorCodes, WizardError } from '@shared/errors'; +import { + AGENT_ERROR_CODE, + classifyRunFailure, + ErrorCodes, + WizardError, +} from '@shared/errors'; +import type { AgentResult } from '../../harness/types'; import type { AgentInteraction } from '@agent/progress'; import type { AgentFailure, @@ -55,7 +61,12 @@ import { TaskStatus, type QueuedTask, } from './queue'; -import { drainQueue, RunTaskFatal, type RunTask } from './executor'; +import { + DEFAULT_DRAIN_OPTIONS, + drainQueue, + RunTaskFatal, + type RunTask, +} from './executor'; import { RunMetrics } from './run-metrics'; import { dependencyClosure, uncoveredBySink } from './queue-tools'; import { deferSeededTasks } from './seeded-deps'; @@ -150,6 +161,45 @@ function requireTaskHarness(pick: HarnessPick): AgentHarness & { }; } +function terminalResult(result: AgentResult): + | { + outcome: RunOutcome.Aborted | RunOutcome.Failed; + failure: AgentFailure; + } + | undefined { + switch (result.kind) { + case 'success': + return undefined; + case 'decided_failure': + return { outcome: RunOutcome.Failed, failure: result.failure }; + case 'abort': + return { + outcome: RunOutcome.Aborted, + failure: { + code: AGENT_ERROR_CODE[result.classification], + message: result.message ?? 'Agent aborted', + error: result.error, + }, + }; + case 'failure': + return { + outcome: RunOutcome.Failed, + failure: { + code: AGENT_ERROR_CODE[result.classification], + message: result.message ?? 'Agent failed', + error: result.error, + }, + }; + } +} + +function cancelledRun(): SequenceResult { + return { + outcome: RunOutcome.Aborted, + failure: { code: ErrorCodes.AgentAbort, message: 'Agent run cancelled' }, + }; +} + /** Every skill entry the menu knows, across categories. */ async function fetchSkillMenuEntries( skillsBaseUrl: string, @@ -205,6 +255,7 @@ export const TASK_NOTICE_TIMEOUT_MS = 5 * 60 * 1000; interface SeededTaskOptions { timeoutMs?: number; interaction?: AgentInteraction; + signal?: AbortSignal; } /** @@ -217,8 +268,13 @@ interface SeededTaskOptions { */ export async function offerSeededTask( notice: TaskNotice, - { timeoutMs = TASK_NOTICE_TIMEOUT_MS, interaction }: SeededTaskOptions = {}, + { + timeoutMs = TASK_NOTICE_TIMEOUT_MS, + interaction, + signal, + }: SeededTaskOptions = {}, ): Promise<{ keep: boolean; timedOut: boolean }> { + if (signal?.aborted) return { keep: false, timedOut: false }; // No one to show the notice to: a step nobody can answer for must not run. // The same answer a non-interactive host gives today. if (!interaction?.taskNotice) return { keep: false, timedOut: false }; @@ -226,20 +282,38 @@ export async function offerSeededTask( let timer: ReturnType | undefined; let timedOut = false; + let cancelForAbort: (() => void) | undefined; const timeout = new Promise((resolve) => { timer = setTimeout(() => { timedOut = true; // Dismisses the overlay and settles the showTaskNotice promise too, so // the losing side of the race cannot leave a modal on screen. - cancelTaskNotice?.(); + try { + cancelTaskNotice?.(); + } catch { + // An overlay failure must not leave a pending notice promise. + } resolve(false); }, timeoutMs); }); + const aborted = new Promise((resolve) => { + cancelForAbort = () => { + try { + cancelTaskNotice?.(); + } catch { + // A host overlay failure must not prevent the run from settling. + } + resolve(false); + }; + signal?.addEventListener('abort', cancelForAbort, { once: true }); + if (signal?.aborted) cancelForAbort(); + }); try { - const keep = await Promise.race([taskNotice(notice), timeout]); + const keep = await Promise.race([taskNotice(notice), timeout, aborted]); return { keep, timedOut }; } finally { if (timer) clearTimeout(timer); + if (cancelForAbort) signal?.removeEventListener('abort', cancelForAbort); } } @@ -431,6 +505,10 @@ export function displayOrder( export async function runOrchestrator( context: SequenceContext, ): Promise { + const controller = new AbortController(); + const abortFromHost = () => controller.abort(); + context.signal?.addEventListener('abort', abortFromHost, { once: true }); + if (context.signal?.aborted) abortFromHost(); let cleaned = false; const cleanupQueue = (): void => { if (cleaned) return; @@ -441,23 +519,34 @@ export async function runOrchestrator( force: true, }); } catch (error) { - analytics.captureException( - error instanceof Error ? error : new Error(String(error)), - { step: 'orchestrator_cache_cleanup' }, - ); + try { + analytics.captureException( + error instanceof Error ? error : new Error(String(error)), + { step: 'orchestrator_cache_cleanup' }, + ); + } catch { + // Cleanup reporting must not replace the run result. + } } }; try { - return await executeOrchestrator(context, cleanupQueue); + return await executeOrchestrator( + { ...context, signal: controller.signal }, + cleanupQueue, + controller, + ); } finally { + context.signal?.removeEventListener('abort', abortFromHost); cleanupQueue(); } } async function executeOrchestrator( - { config, input, boot, emit, interaction }: SequenceContext, + { config, input, boot, emit, interaction, signal }: SequenceContext, cleanupQueue: () => void, + controller: AbortController, ): Promise { + if (signal?.aborted) return cancelledRun(); const runId = randomUUID(); const { run } = config; const programId = config.programId; @@ -480,6 +569,7 @@ async function executeOrchestrator( boot.wizardFlagPayloads, ), }); + if (signal?.aborted) return cancelledRun(); const seedPrompt = registry.seed; if (!seedPrompt) { throw new Error( @@ -587,6 +677,7 @@ async function executeOrchestrator( let commandmentsPath: string | undefined; let referenceInstallPath: string | undefined; const menuSkillEntries = await fetchSkillMenuEntries(boot.skillsBaseUrl); + if (signal?.aborted) return cancelledRun(); // The framework key for reference + variant resolution. `input.integration` // is the detected framework and always wins; `input.skillId` is the // fallback for the basic-integration path, where the caller sets it to the @@ -607,6 +698,7 @@ async function executeOrchestrator( triage: boot.triageProvider, }, ); + if (signal?.aborted) return cancelledRun(); if (ref.kind === 'ok') { referenceInstallPath = ref.path; const example = path.join(ref.path, 'references', 'EXAMPLE.md'); @@ -779,8 +871,12 @@ async function executeOrchestrator( // not, which is why the offer lives here and not there. seededConsent.set( task.id, - await askSeededConsent(seeded.type, seeded.notice, { interaction }), + await askSeededConsent(seeded.type, seeded.notice, { + interaction, + signal, + }), ); + if (signal?.aborted) return cancelledRun(); } logToFile(`[orchestrator] runner-seeded task ${seeded.type}`); } @@ -814,6 +910,7 @@ async function executeOrchestrator( const askBridge = shouldDisableAsk(input.flags) ? undefined : createAskBridge(interaction, { + signal, getSource: () => input.skillId ?? programId, beforeShow: () => { // How late the first ask lands is the measure of this run shape: it @@ -845,6 +942,7 @@ async function executeOrchestrator( const seedHarness = requireTaskHarness(seedPick); const seedModel = promptModelFor(seedPrompt, seedPick.harness); const seedResult = await seedHarness.runTask({ + signal, config, input, boot, @@ -861,15 +959,9 @@ async function executeOrchestrator( requestRemark: false, analyticsProperties: { task_type: 'seed', harness: seedPick.harness }, }); - // A decided seed failure ends the run and releases its queue artifacts. - if (seedResult.failure) return failed(seedResult.failure); - if (seedResult.error) { - logToFile( - `[orchestrator] seed error: ${seedResult.error} ${ - seedResult.message ?? '' - }`, - ); - } + if (signal?.aborted) return cancelledRun(); + const seedTerminal = terminalResult(seedResult); + if (seedTerminal) return seedTerminal; analytics.wizardCapture('orchestrator seeded', { task_count: store.list().length, types: store.list().map((t) => t.type), @@ -992,6 +1084,7 @@ async function executeOrchestrator( existsSync(claudeSkillsDir) ? readdirSync(claudeSkillsDir) : [], ); const runTask: RunTask = async (task) => { + if (signal?.aborted) return; renderQueue(); try { @@ -1002,6 +1095,7 @@ async function executeOrchestrator( // The prompt points the agent at them instead. const skillPaths: string[] = []; for (const skillId of resolved.skills) { + if (signal?.aborted) return; // Agent prompts name the bare step-skill (`integration-v2-install`); // SDK-divergent steps ship per-framework variants, so resolve against // the menu with the session's framework before installing. @@ -1024,6 +1118,7 @@ async function executeOrchestrator( boot.skillsBaseUrl, { skillsRoot: taskSkillsRoot, triage: boot.triageProvider }, ); + if (signal?.aborted) return; if (result.kind === 'ok') { skillPaths.push(path.join(result.path, 'SKILL.md')); } else { @@ -1051,33 +1146,49 @@ async function executeOrchestrator( const taskPick = resolveHarness(switchboardCtx, task.type); const taskHarness = requireTaskHarness(taskPick); const taskModel = taskModelSpec(registry, task, taskPick.harness); - const taskResult = await taskHarness.runTask({ - config, - input, - boot, - emit, - prompt: assembleTaskPrompt(promptContext, resolved.prompt, skillPaths), - spinner, - model: requireKnownModel(taskModel.model, taskPick.model), - effort: taskModel.effort, - allowedTools: resolved.allowedTools, - disallowedTools: resolved.disallowedTools, - askBridge: canAsk(registry.get(task.type)) ? askBridge : undefined, - orchestrator: orchestratorCtx(task.id), - spinnerMessage: '', - successMessage: '', - additionalFeatureQueue: [], - requestRemark: false, - analyticsProperties: { - task_type: task.type, - task_id: task.id, - harness: taskPick.harness, - }, - }); - // A decided failure (a 401 the harness already reported) is the run's, - // not the task's: stop the drain and report it, where the harness used - // to exit the process. - if (taskResult.failure) throw new RunTaskFatal(taskResult.failure); + let taskResult: AgentResult; + try { + taskResult = await taskHarness.runTask({ + signal, + config, + input, + boot, + emit, + prompt: assembleTaskPrompt( + promptContext, + resolved.prompt, + skillPaths, + ), + spinner, + model: requireKnownModel(taskModel.model, taskPick.model), + effort: taskModel.effort, + allowedTools: resolved.allowedTools, + disallowedTools: resolved.disallowedTools, + askBridge: canAsk(registry.get(task.type)) ? askBridge : undefined, + orchestrator: orchestratorCtx(task.id), + spinnerMessage: '', + successMessage: '', + additionalFeatureQueue: [], + requestRemark: false, + analyticsProperties: { + task_type: task.type, + task_id: task.id, + harness: taskPick.harness, + }, + }); + } catch (error) { + if (signal?.aborted) return; + if (error instanceof RunTaskFatal) throw error; + const failure = classifyRunFailure(error); + throw new RunTaskFatal({ + code: failure.code, + message: failure.message, + error: error instanceof Error ? error : undefined, + }); + } + if (signal?.aborted) return; + const terminal = terminalResult(taskResult); + if (terminal) throw new RunTaskFatal(terminal.failure, terminal.outcome); } finally { // Durable skills a task installed are irrelevant to later tasks — and // the sdk harness auto-loads .claude/skills into every agent — so sweep @@ -1089,9 +1200,21 @@ async function executeOrchestrator( referenceSkillId, ); } catch (err) { - logToFile(`[orchestrator] per-task skill sweep failed: ${String(err)}`); + try { + logToFile('[orchestrator] per-task skill sweep failed:', err); + } catch { + // Cleanup logging must not replace the task result. + } + } + try { + renderQueue(); + } catch (err) { + try { + logToFile('[orchestrator] per-task queue render failed:', err); + } catch { + // Cleanup logging must not replace the task result. + } } - renderQueue(); } }; // A task that stops for the user is offered, not imposed, and the answer was @@ -1101,15 +1224,19 @@ async function executeOrchestrator( renderQueue(); } - let fatal: AgentFailure | undefined; + let fatal: RunTaskFatal | undefined; try { - await drainQueue(store, runTask); + await drainQueue(store, runTask, { + ...DEFAULT_DRAIN_OPTIONS, + signal, + onFatal: () => controller.abort(), + }); } catch (error) { if (!(error instanceof RunTaskFatal)) throw error; - fatal = error.failure; + fatal = error; } finally { try { - if (referenceSkillId && referenceInstallPath) { + if (!signal?.aborted && referenceSkillId && referenceInstallPath) { promoteReferenceSkill( path.join(input.installDir, referenceInstallPath), claudeSkillsDir, @@ -1117,10 +1244,14 @@ async function executeOrchestrator( ); } } catch (err) { - analytics.captureException( - err instanceof Error ? err : new Error(String(err)), - { step: 'orchestrator_reference_promote' }, - ); + try { + analytics.captureException( + err instanceof Error ? err : new Error(String(err)), + { step: 'orchestrator_reference_promote' }, + ); + } catch { + // Cleanup reporting must not replace the run result. + } } cleanupQueue(); try { @@ -1130,14 +1261,19 @@ async function executeOrchestrator( referenceSkillId, ); } catch (err) { - analytics.captureException( - err instanceof Error ? err : new Error(String(err)), - { step: 'orchestrator_skill_sweep' }, - ); + try { + analytics.captureException( + err instanceof Error ? err : new Error(String(err)), + { step: 'orchestrator_skill_sweep' }, + ); + } catch { + // Cleanup reporting must not replace the run result. + } } } - if (fatal) return failed(fatal); + if (fatal) return { outcome: fatal.outcome, failure: fatal.failure }; + if (signal?.aborted) return cancelledRun(); renderQueue(); diff --git a/src/agent/runner/shared/ask.ts b/src/agent/runner/shared/ask.ts index ffe178940..af1be4729 100644 --- a/src/agent/runner/shared/ask.ts +++ b/src/agent/runner/shared/ask.ts @@ -21,6 +21,7 @@ export function createAskBridge( getSource: () => string; richLinks: boolean; timeoutMs?: number; + signal?: AbortSignal; /** Runs before each question is shown (the orchestrator's bell and metric). */ beforeShow?: () => void; }, @@ -37,5 +38,6 @@ export function createAskBridge( cancelQuestion: interaction?.cancelAsk, richLinks: options.richLinks, timeoutMs: options.timeoutMs, + signal: options.signal, }); } diff --git a/src/agent/runner/shared/errors.ts b/src/agent/runner/shared/errors.ts index d89d3c214..d227a7f0f 100644 --- a/src/agent/runner/shared/errors.ts +++ b/src/agent/runner/shared/errors.ts @@ -3,8 +3,7 @@ */ import type { InstallSkillResult } from '@agent/tools'; -import { skillErrorCode } from '@shared/errors'; -import { WizardError } from '@shared/errors'; +import { ErrorCodes, skillErrorCode } from '@shared/errors'; import { RunOutcome, type AgentFailure, type SequenceResult } from './types'; export const failed = (failure: AgentFailure): SequenceResult => ({ @@ -17,7 +16,7 @@ export function installFailure( integrationLabel: string, result: Exclude, ): AgentFailure { - const code = skillErrorCode(result) ?? undefined; + const code = skillErrorCode(result) ?? ErrorCodes.InternalUnhandled; const message = (() => { switch (result.kind) { @@ -33,17 +32,13 @@ export function installFailure( return { message, code, - error: new WizardError( - `Skill install failed: ${result.kind}`, - { - integration: integrationLabel, - error_type: result.kind, - platform: process.platform, - ...(result.kind === 'download-failed' - ? { error_detail: result.message.slice(0, 500) } - : {}), - }, - code, - ), + detail: { + integration: integrationLabel, + error_type: result.kind, + platform: process.platform, + ...(result.kind === 'download-failed' + ? { error_detail: result.message.slice(0, 500) } + : {}), + }, }; } diff --git a/src/agent/runner/shared/progress-collector.ts b/src/agent/runner/shared/progress-collector.ts index 719603e63..98b6e5154 100644 --- a/src/agent/runner/shared/progress-collector.ts +++ b/src/agent/runner/shared/progress-collector.ts @@ -23,7 +23,7 @@ export interface ProgressCollector { } export function createProgressCollector( - onProgress?: (event: AgentProgress) => void, + onProgress?: (event: AgentProgress) => unknown, ): ProgressCollector { const snapshot: RunSnapshot = { tasks: [], @@ -75,14 +75,33 @@ export function createProgressCollector( apply(event); if (!onProgress) return; try { - onProgress(structuredClone(event)); + const observed = onProgress(structuredClone(event)); + if ( + observed && + typeof (observed as PromiseLike).then === 'function' + ) { + void Promise.resolve(observed).catch((error: unknown) => { + try { + logToFile( + `[agent] progress observer rejected on ${event.kind}:`, + error, + ); + } catch { + // Logging is best effort. + } + }); + } } catch (error) { // A broken projection is the host's problem, not the run's. Say so in // the log and carry on; the snapshot above is the source of truth. - logToFile( - `[agent] progress observer threw on ${event.kind}:`, - error instanceof Error ? error.message : error, - ); + try { + logToFile( + `[agent] progress observer threw on ${event.kind}:`, + error instanceof Error ? error.message : error, + ); + } catch { + // Logging is best effort. + } } }; diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index 3f97d5f5d..2907f2eaf 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -12,7 +12,7 @@ import type { AdditionalFeature } from '@shared/constants'; import type { CloudRegion } from '@utils/types'; import type { Credentials } from '@shared/api'; -import type { OutroData, TaskNotice } from '@agent/progress'; +import type { AuthErrorDetail, OutroData, TaskNotice } from '@agent/progress'; import type { PromptContext } from '@agent/agent-prompt'; import type { PackageManagerDetector } from '@utils/package-manager'; import type { ApiProject, ApiUser } from '@shared/api'; @@ -246,13 +246,14 @@ export interface BootstrapResult { * messages stay exactly what they were. */ export interface AgentFailure { - message?: string; + message: string; /** Structured error data. Renders via `outroError` instead of `outro`. */ outroData?: OutroData; error?: Error; exitCode?: number; - code?: ErrorCode; + code: ErrorCode; detail?: Record; + authErrorDetail?: AuthErrorDetail; } export enum RunOutcome { @@ -308,9 +309,10 @@ export type RunResult = ( export interface RunAgentOptions { /** Receives every progress event in emission order. Never awaited. */ - onProgress?: (event: import('@agent/progress').AgentProgress) => void; + onProgress?: (event: import('@agent/progress').AgentProgress) => unknown; /** Answers the agent's questions. Absent → no ask bridge, notices declined. */ interaction?: AgentInteraction; + signal?: AbortSignal; } /** What a sequence receives: the contracts plus the prepared run. */ @@ -320,4 +322,5 @@ export interface SequenceContext { boot: BootstrapResult; emit: ProgressEmitter; interaction: AgentInteraction | undefined; + signal?: AbortSignal; } diff --git a/src/agent/wizard-ask-bridge.ts b/src/agent/wizard-ask-bridge.ts index 68de191e5..505a51cd8 100644 --- a/src/agent/wizard-ask-bridge.ts +++ b/src/agent/wizard-ask-bridge.ts @@ -54,6 +54,7 @@ export interface WizardAskBridge { } export interface WizardAskBridgeOptions { + signal?: AbortSignal; /** Returns the active skill id, used as the analytics `source` on the request. */ getSource: () => string; /** Opens the overlay and resolves once the user submits or cancels. */ @@ -120,6 +121,9 @@ export function createWizardAskBridge( return null; }, async request({ questions, subject }) { + if (opts.signal?.aborted) { + return { answers: buildCancelledAnswers(questions), timedOut: false }; + } const pending: PendingQuestion = { id: randomUUID(), questions, @@ -132,6 +136,7 @@ export function createWizardAskBridge( const startedAt = Date.now(); let timer: ReturnType | undefined; let timedOut = false; + let cancelForAbort: (() => void) | undefined; // Race the user against the timeout. Whichever fires first wins. On // timeout we also cancel the host's overlay: resolving our side alone @@ -140,15 +145,33 @@ export function createWizardAskBridge( const timeoutPromise = new Promise((resolve) => { timer = setTimeout(() => { timedOut = true; - opts.cancelQuestion?.(); - resolve(buildCancelledAnswers(questions)); + try { + opts.cancelQuestion?.(); + } catch { + // A host overlay failure must not leave the ask pending. + } finally { + resolve(buildCancelledAnswers(questions)); + } }, timeoutMs); }); + const aborted = new Promise((resolve) => { + cancelForAbort = () => { + try { + opts.cancelQuestion?.(); + } catch { + // A host overlay failure must not prevent the run from settling. + } + resolve(buildCancelledAnswers(questions)); + }; + opts.signal?.addEventListener('abort', cancelForAbort, { once: true }); + if (opts.signal?.aborted) cancelForAbort(); + }); try { const answers = await Promise.race([ opts.showQuestion(pending), timeoutPromise, + aborted, ]); const durationMs = Date.now() - startedAt; @@ -172,6 +195,8 @@ export function createWizardAskBridge( return { answers, timedOut }; } finally { if (timer) clearTimeout(timer); + if (cancelForAbort) + opts.signal?.removeEventListener('abort', cancelForAbort); pendingQuestions.delete(pending.id); } }, diff --git a/src/lib/detection/__tests__/agentic-progress.test.ts b/src/lib/detection/__tests__/agentic-progress.test.ts index 41e7f0761..424c98414 100644 --- a/src/lib/detection/__tests__/agentic-progress.test.ts +++ b/src/lib/detection/__tests__/agentic-progress.test.ts @@ -6,6 +6,7 @@ import { import { buildSession } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { getUI } from '@ui'; +import { ErrorCodes } from '@shared/errors'; vi.mock('@utils/debug'); vi.mock('@ui', () => ({ getUI: () => ui })); @@ -13,6 +14,7 @@ const ui = vi.hoisted(() => ({ addTokenUsage: vi.fn(), setStage: vi.fn(), pushStatus: vi.fn(), + showAuthError: vi.fn(), log: { error: vi.fn() }, })); vi.mock('@agent/agent-interface', async (original) => ({ @@ -55,7 +57,7 @@ it('keeps initialization and execution progress visible during detection', async result: '{"projects":[{"path":".","targetId":"node","framework":"Node.js"}]}', }); - return Promise.resolve({}); + return Promise.resolve({ kind: 'success' }); }, ); const session = buildSession({ installDir: '/tmp/detection-test' }); @@ -78,3 +80,38 @@ it('keeps initialization and execution progress visible during detection', async ['Execution diagnostic'], ]); }); + +it('stops optional detection on a data-only 401 before parsing partial JSON', async () => { + vi.mocked(initializeAgent).mockResolvedValue( + {} as Awaited>, + ); + vi.mocked(executeAgent).mockImplementation( + (_config, _prompt, _options, _spinner, _messages, middleware) => { + middleware?.onMessage({ + type: 'result', + result: '{"projects":[{"path":".","targetId":"node"}]}', + }); + return Promise.resolve({ + kind: 'decided_failure', + failure: { + code: ErrorCodes.AuthInvalidOrExpired, + message: 'Authentication failed (401)', + }, + }); + }, + ); + const session = buildSession({ installDir: '/tmp/detection-test' }); + session.credentials = { + accessToken: 'test', + projectApiKey: 'phc_test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }; + await expect( + detectProjectsWithAgent(session, { + programId: 'posthog-integration', + targets: [{ id: 'node', name: 'Node.js' }], + }), + ).rejects.toThrow('Authentication failed (401)'); + expect(ui.showAuthError).not.toHaveBeenCalled(); +}); diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index e3f96aa22..f32d851fd 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -430,8 +430,14 @@ export async function detectProjectsWithAgent( middleware, ); - if (result.error) { - throw new Error(result.message || `Agent error: ${result.error}`); + if (result.kind !== 'success') { + if (result.kind === 'decided_failure') { + throw result.failure.error ?? new Error(result.failure.message); + } + throw ( + result.error ?? + new Error(result.message || `Agent error: ${result.classification}`) + ); } // Transcript first, final message last — its verdicts win path conflicts. diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index 1fdce1e41..c916085a5 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -6,10 +6,11 @@ import { Harness, Sequence } from '@shared/constants'; import { buildSession, OutroKind } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { LoggingUI } from '@ui/logging-ui'; -import { setUI } from '@ui'; +import { getUI, setUI } from '@ui'; import { analytics } from '@utils/analytics'; import { initLogFile } from '@utils/debug'; import { wizardAbort } from '@utils/wizard-abort'; +import { ErrorCodes } from '@shared/errors'; import type { ProgramConfig } from '../program-step'; const streamShutdown = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); @@ -171,7 +172,11 @@ it('clamps a composed program to linear and keeps host analytics alive', async ( it.each([RunOutcome.Aborted, RunOutcome.Failed] as const)( 'passes a %s result to the existing abort handler', async (outcome) => { - const failure = { message: 'Failed', exitCode: 2 }; + const failure = { + code: ErrorCodes.AgentApiError, + message: 'Failed', + exitCode: 2, + }; vi.mocked(runAgent).mockResolvedValue({ outcome, failure, snapshot }); await runProgramAgent(program(), session()); expect(wizardAbort).toHaveBeenCalledExactlyOnceWith(failure); @@ -179,11 +184,34 @@ it.each([RunOutcome.Aborted, RunOutcome.Failed] as const)( }, ); +it('shows the auth guidance from a decided 401 before the error outro', async () => { + const detail = { hasSettingsConflict: false, logFilePath: '/tmp/wizard.log' }; + const show = vi.spyOn(getUI(), 'showAuthError'); + vi.mocked(runAgent).mockResolvedValue({ + outcome: RunOutcome.Failed, + failure: { + code: ErrorCodes.AuthInvalidOrExpired, + message: 'Authentication failed (401)', + authErrorDetail: detail, + }, + snapshot, + }); + await runProgramAgent(program(), session()); + expect(show).toHaveBeenCalledExactlyOnceWith(detail); + expect(wizardAbort).toHaveBeenCalledWith( + expect.objectContaining({ authErrorDetail: detail }), + ); +}); + it('rethrows the original crash for the outer runner', async () => { const error = new Error('mint refused'); const result: RunResult = { outcome: RunOutcome.Crashed, - failure: { error }, + failure: { + code: ErrorCodes.InternalUnhandled, + message: error.message, + error, + }, snapshot, }; vi.mocked(runAgent).mockResolvedValue(result); diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index e0c0733d0..2eb076c2b 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -282,6 +282,9 @@ async function runProgram( throw result.failure.error; } if (result.outcome !== RunOutcome.Success) { + if (result.failure.authErrorDetail) { + ui.showAuthError(result.failure.authErrorDetail); + } await wizardAbort(result.failure); } } From 1a040d00fd17e4ddbb367ee6c97786be5055d169 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 21:21:55 -0400 Subject: [PATCH 35/61] Propagate agentic detection failures --- .../__tests__/agentic-progress.test.ts | 94 +++++++++++++++++-- src/lib/detection/agentic.ts | 11 +++ 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/lib/detection/__tests__/agentic-progress.test.ts b/src/lib/detection/__tests__/agentic-progress.test.ts index 41e7f0761..32daece98 100644 --- a/src/lib/detection/__tests__/agentic-progress.test.ts +++ b/src/lib/detection/__tests__/agentic-progress.test.ts @@ -1,10 +1,12 @@ import { detectProjectsWithAgent } from '../agentic'; import { + AgentErrorType, initializeAgent, runAgent as executeAgent, } from '@agent/agent-interface'; import { buildSession } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; +import { ErrorCodes } from '@shared/errors'; import { getUI } from '@ui'; vi.mock('@utils/debug'); @@ -21,6 +23,22 @@ vi.mock('@agent/agent-interface', async (original) => ({ runAgent: vi.fn(), })); +function detectionSession() { + const session = buildSession({ installDir: '/tmp/detection-test' }); + session.credentials = { + accessToken: 'test', + projectApiKey: 'phc_test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }; + return session; +} + +beforeEach(() => { + vi.mocked(initializeAgent).mockReset(); + vi.mocked(executeAgent).mockReset(); +}); + it('keeps initialization and execution progress visible during detection', async () => { const delta = { inputTokens: 5, @@ -58,14 +76,7 @@ it('keeps initialization and execution progress visible during detection', async return Promise.resolve({}); }, ); - const session = buildSession({ installDir: '/tmp/detection-test' }); - session.credentials = { - accessToken: 'test', - projectApiKey: 'phc_test', - projectId: 1, - host: HostResolution.fromApiHost('https://us.posthog.com'), - }; - const report = await detectProjectsWithAgent(session, { + const report = await detectProjectsWithAgent(detectionSession(), { programId: 'posthog-integration', targets: [{ id: 'node', name: 'Node.js' }], }); @@ -78,3 +89,70 @@ it('keeps initialization and execution progress visible during detection', async ['Execution diagnostic'], ]); }); + +it('rejects a decided failure even when the transcript contains a report', async () => { + const original = new Error('Gateway bearer rejected'); + vi.mocked(initializeAgent).mockResolvedValue( + {} as Awaited>, + ); + vi.mocked(executeAgent).mockImplementation( + (_config, _prompt, _options, _spinner, _messages, middleware) => { + middleware?.onMessage({ + type: 'result', + result: + '{"projects":[{"path":".","targetId":"node","framework":"Node.js"}]}', + }); + return Promise.resolve({ failure: { error: original } }); + }, + ); + + await expect( + detectProjectsWithAgent(detectionSession(), { + programId: 'posthog-integration', + targets: [{ id: 'node', name: 'Node.js' }], + }), + ).rejects.toBe(original); +}); + +it.each([ + { message: 'Gateway token refused', expected: 'Gateway token refused' }, + { message: undefined, expected: 'Agent detection failed' }, +])( + 'keeps the code and message of a failure without an Error', + async ({ message, expected }) => { + vi.mocked(initializeAgent).mockResolvedValue( + {} as Awaited>, + ); + vi.mocked(executeAgent).mockResolvedValue({ + failure: { code: ErrorCodes.GatewayMintRefused, message }, + }); + + await expect( + detectProjectsWithAgent(detectionSession(), { + programId: 'posthog-integration', + targets: [{ id: 'node', name: 'Node.js' }], + }), + ).rejects.toMatchObject({ + name: 'WizardError', + message: expected, + code: ErrorCodes.GatewayMintRefused, + }); + }, +); + +it('continues to reject legacy agent errors', async () => { + vi.mocked(initializeAgent).mockResolvedValue( + {} as Awaited>, + ); + vi.mocked(executeAgent).mockResolvedValue({ + error: AgentErrorType.API_ERROR, + message: 'Agent API unavailable', + }); + + await expect( + detectProjectsWithAgent(detectionSession(), { + programId: 'posthog-integration', + targets: [{ id: 'node', name: 'Node.js' }], + }), + ).rejects.toThrow('Agent API unavailable'); +}); diff --git a/src/lib/detection/agentic.ts b/src/lib/detection/agentic.ts index e3f96aa22..2799b50ba 100644 --- a/src/lib/detection/agentic.ts +++ b/src/lib/detection/agentic.ts @@ -27,6 +27,7 @@ import type { WizardSession } from '@lib/wizard-session'; import type { WizardRunOptions } from '@utils/types'; import { getUI, type SpinnerHandle } from '@ui'; import { createUiReducer } from '@ui/agent-progress'; +import { WizardError } from '@shared/errors'; /** A category the agent classifies each project into (id the agent returns). */ export type DetectTarget = { id: string; name: string }; @@ -430,6 +431,16 @@ export async function detectProjectsWithAgent( middleware, ); + if (result.failure) { + throw ( + result.failure.error ?? + new WizardError( + result.failure.message ?? 'Agent detection failed', + undefined, + result.failure.code, + ) + ); + } if (result.error) { throw new Error(result.message || `Agent error: ${result.error}`); } From 1443587cd5e3cc0c5c6d8a824d17cba67be7452d Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 21:26:52 -0400 Subject: [PATCH 36/61] test(agent): probe terminal SDK fault propagation Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- scripts/a3-fault-probe.no-jest.ts | 119 ++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 scripts/a3-fault-probe.no-jest.ts diff --git a/scripts/a3-fault-probe.no-jest.ts b/scripts/a3-fault-probe.no-jest.ts new file mode 100644 index 000000000..91b83e5ac --- /dev/null +++ b/scripts/a3-fault-probe.no-jest.ts @@ -0,0 +1,119 @@ +import type { RunConfig, RunInput } from '@agent/runner'; + +const gatewayUrl = process.env.WIZARD_FAULT_GATEWAY_URL; +const installDir = process.env.WIZARD_FAULT_INSTALL_DIR; +const harness = process.env.WIZARD_FAULT_HARNESS; +if ( + !gatewayUrl || + !installDir || + !['anthropic', 'pi'].includes(harness ?? '') +) { + throw new Error( + 'Expected WIZARD_FAULT_GATEWAY_URL, WIZARD_FAULT_INSTALL_DIR, and WIZARD_FAULT_HARNESS', + ); +} + +const routedFetch = globalThis.fetch; +globalThis.fetch = (input, init) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + if (url.hostname === 'internal-j.posthog.com') { + return Promise.resolve(new Response('{"status":1}', { status: 200 })); + } + if (url.hostname !== '127.0.0.1' && url.hostname !== 'localhost') { + return Promise.reject( + new Error('Fault probe blocked a non-loopback fetch'), + ); + } + return routedFetch(input, init); +}; + +const { runAgent } = await import('@agent/runner'); +const { configureGatewayCredentialsForCI } = await import( + '@agent/gateway-session' +); +const { DEFAULT_AGENT_MODEL, Harness, Sequence } = await import( + '@shared/constants' +); +const { HostResolution } = await import('@shared/host-resolution'); +const { analytics } = await import('@utils/analytics'); + +// This probe has no telemetry sink and uses only synthetic local credentials. +analytics.capture = () => {}; +analytics.captureException = () => {}; +analytics.wizardCapture = () => {}; +analytics.shutdown = async () => {}; + +configureGatewayCredentialsForCI( + 'phe_synthetic_fault_probe', + 228144, + gatewayUrl, +); + +const config: RunConfig = { + programId: 'fault-probe', + run: { + integrationLabel: 'Fault probe', + spinnerMessage: 'Running fault probe', + successMessage: 'Fault probe completed', + estimatedDurationMinutes: 1, + reportFile: 'fault-probe.md', + docsUrl: 'https://posthog.com/docs', + customPrompt: () => 'Answer briefly without using tools.', + }, + composed: false, + binding: { + sequence: Sequence.linear, + harness: harness as Harness, + model: DEFAULT_AGENT_MODEL, + }, + switchboard: { + program: 'fault-probe', + flags: {}, + cliHarness: harness as Harness, + }, + skillsBaseUrl: 'http://127.0.0.1:1', + wizardFlags: {}, + wizardFlagPayloads: {}, + wizardMetadata: { run_id: 'fault-probe' }, +}; + +const input: RunInput = { + installDir, + credentials: { + accessToken: 'phx_synthetic_fault_probe', + projectApiKey: 'phc_synthetic_fault_probe', + host: HostResolution.fromApiHost('http://127.0.0.1:1', { localMcp: true }), + projectId: 228144, + }, + project: null, + apiUser: null, + flags: { + ci: true, + signup: false, + debug: false, + e2eAsk: false, + localMcp: true, + captureAio: false, + benchmark: false, + yaraReport: false, + }, + host: { projectId: 228144, region: 'us' }, +}; + +const result = await runAgent(config, input); +process.stdout.write( + `WIZARD_FAULT_RESULT ${JSON.stringify({ + harness, + outcome: result.outcome, + code: result.failure?.code, + message: result.failure?.message, + hasError: result.failure?.error instanceof Error, + outroKind: result.outro?.kind, + })}\n`, +); +if (result.outcome === 'failed' || result.outcome === 'aborted') { + const { wizardAbort } = await import('@utils/wizard-abort'); + await wizardAbort(result.failure); +} else { + process.exitCode = 2; +} From 4f1940d12c8e25406cded859b4908d34385ab41f Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 21:26:53 -0400 Subject: [PATCH 37/61] Fix program ownership and architecture boundaries --- .eslintrc.cjs | 2 +- .github/CODEOWNERS | 26 ++++++++++++------- README.md | 9 ++++++- .../architecture/import-boundaries.test.ts | 21 ++++++++++++--- .../architecture/known-violations.json | 3 +-- src/lib/wizard-session.ts | 2 +- src/lib/wizard-spellbook.ts | 2 +- 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 3a1795f54..fe3b3c1d7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -82,7 +82,7 @@ module.exports = { 'The agent reports through progress events and asks through AgentInteraction; it takes everything else through RunConfig and RunInput.', }, { - group: ['@programs/**', '**/programs/**'], + group: ['@programs', '@programs/**', '**/programs/**'], allowTypeImports: true, message: 'The agent takes program data through RunConfig. ProgramId types remain until B2 moves PROGRAM_BINDINGS to programs.', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 48f68a2a9..7bceb017f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,16 +8,22 @@ * @PostHog/team-wizard-docs # Team-owned programs -/src/lib/programs/ai-observability/ @PostHog/team-ai-observability -/src/lib/programs/error-tracking-upload-source-maps/ @PostHog/team-error-tracking -/src/lib/programs/mcp-analytics/ @PostHog/team-mcp-analytics -/src/lib/programs/metrics/ @PostHog/apm -/src/lib/programs/posthog-integration/ @PostHog/team-wizard-docs -/src/lib/programs/replay-vision/ @PostHog/team-replay -/src/lib/programs/revenue-analytics/ @PostHog/team-web-analytics -/src/lib/programs/self-driving/ @PostHog/team-self-driving -/src/lib/programs/warehouse-source/ @PostHog/team-warehouse-sources -/src/lib/programs/web-analytics-doctor/ @PostHog/team-web-analytics +/src/programs/ai-observability/ @PostHog/team-ai-observability +/src/programs/error-tracking-upload-source-maps/ @PostHog/team-error-tracking +/src/programs/mcp-analytics/ @PostHog/team-mcp-analytics +/src/programs/metrics/ @PostHog/apm +/src/programs/posthog-integration/ @PostHog/team-wizard-docs +/src/programs/replay-vision/ @PostHog/team-replay +/src/programs/revenue-analytics/ @PostHog/team-web-analytics +/src/programs/self-driving/ @PostHog/team-self-driving +/src/programs/warehouse-source/ @PostHog/team-warehouse-sources +/src/programs/web-analytics-doctor/ @PostHog/team-web-analytics + +# Team-owned program decks +/src/ui/tui/decks/error-tracking-upload-source-maps/ @PostHog/team-error-tracking +/src/ui/tui/decks/revenue-analytics/ @PostHog/team-web-analytics +/src/ui/tui/decks/self-driving/ @PostHog/team-self-driving +/src/ui/tui/decks/warehouse-source/ @PostHog/team-warehouse-sources # Agent runner (harness, model, and tools) /src/agent/ @PostHog/team-wizard-docs diff --git a/README.md b/README.md index be133cf7b..d1470fd36 100644 --- a/README.md +++ b/README.md @@ -149,20 +149,27 @@ npx @posthog/wizard@latest skill # run one by name Reviews are auto-requested via [`.github/CODEOWNERS`](.github/CODEOWNERS) — the file is the source of truth; this table just mirrors it for readability. -`team-wizard-docs` is the default reviewer; the team-owned programs below +`team-wizard-docs` is the default reviewer; the team-owned paths below route review to their owning team instead. | Path | Owning team | |---|---| | `*` (everything else, including all other programs) | `@PostHog/team-wizard-docs` | | `src/agent/` | `@PostHog/team-wizard-docs` | +| `src/programs/ai-observability/` | `@PostHog/team-ai-observability` | | `src/programs/posthog-integration/` | `@PostHog/team-wizard-docs` | | `src/programs/error-tracking-upload-source-maps/` | `@PostHog/team-error-tracking` | | `src/programs/mcp-analytics/` | `@PostHog/team-mcp-analytics` | +| `src/programs/metrics/` | `@PostHog/apm` | +| `src/programs/replay-vision/` | `@PostHog/team-replay` | | `src/programs/revenue-analytics/` | `@PostHog/team-web-analytics` | | `src/programs/self-driving/` | `@PostHog/team-self-driving` | | `src/programs/warehouse-source/` | `@PostHog/team-warehouse-sources` | | `src/programs/web-analytics-doctor/` | `@PostHog/team-web-analytics` | +| `src/ui/tui/decks/error-tracking-upload-source-maps/` | `@PostHog/team-error-tracking` | +| `src/ui/tui/decks/revenue-analytics/` | `@PostHog/team-web-analytics` | +| `src/ui/tui/decks/self-driving/` | `@PostHog/team-self-driving` | +| `src/ui/tui/decks/warehouse-source/` | `@PostHog/team-warehouse-sources` | Ownership is by directory. Programs not listed above (`agent-skill`, `audit`, `events-audit`, `mcp`, `migration`, `posthog-doctor`, diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index c53cc623a..e4b566abd 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -23,8 +23,7 @@ const SURFACE_RULES: ReadonlyArray boolean]> = [ 'tui', (p) => - p.startsWith('src/ui/') || - p.startsWith('src/steps/') || + p.startsWith('src/ui/tui/') || p === 'src/commands/factories/family-picker.tsx', ], [ @@ -419,8 +418,8 @@ describe('surface classification', () => { 'programs', ); expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); - expect(classifySurface('src/ui/index.ts')).toBe('tui'); - expect(classifySurface('src/steps/index.ts')).toBe('tui'); + expect(classifySurface('src/ui/index.ts')).toBe('legacy'); + expect(classifySurface('src/steps/index.ts')).toBe('legacy'); expect(classifySurface('bin.ts')).toBe('cli'); expect(classifySurface('src/agent/tools/mcp.ts')).toBe('agent'); expect(classifySurface('src/agent/tools/tools.ts')).toBe('agent'); @@ -501,3 +500,17 @@ describe('programs entry modules', () => { ).toBe('matrix:programs->cli'); }); }); + +describe('migration matrix', () => { + it('lets legacy code use the programs entry', () => { + expect(ruleFor('src/lib/wizard-session.ts', 'src/programs/index.ts')).toBe( + null, + ); + }); + + it('keeps agent code out of legacy session state', () => { + expect( + ruleFor('src/agent/runner/index.ts', 'src/lib/wizard-session.ts'), + ).toBe('matrix:agent->legacy'); + }); +}); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 9273dfd7f..0d110b9d3 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -35,8 +35,6 @@ "src/lib/runners/run-wizard.ts -> src/programs/task-stream/index.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/task-stream-push.ts", "src/lib/wizard-session.ts -> src/agent/progress.ts", - "src/lib/wizard-session.ts -> src/programs/framework-config.ts", - "src/lib/wizard-spellbook.ts -> src/programs/program-step.ts", "src/programs/agent-skill/index.ts -> src/ui/tui/decks/agent-skill/index.tsx", "src/programs/agent-skill/steps.ts -> src/lib/wizard-session.ts", "src/programs/ai-observability/index.ts -> src/lib/headless-mode.ts", @@ -133,6 +131,7 @@ "src/shared/utils/wizard-abort.ts -> src/lib/wizard-session.ts", "src/shared/utils/wizard-abort.ts -> src/ui/index.ts", "src/shared/utils/wizard-abort.ts -> src/ui/logging-ui.ts", + "src/ui/headless-ui.ts -> src/ui/tui/store.ts", "src/ui/tui/decks/error-tracking/tips.ts -> src/programs/replay-vision/index.ts", "src/ui/tui/playground/demos/AuditChecksDemo.tsx -> src/programs/audit/types.ts", "src/ui/tui/playground/demos/DoctorReportDemo.tsx -> src/programs/posthog-doctor/index.ts", diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 7d3fbf5ba..4739c3aa6 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -19,7 +19,7 @@ import { type Integration, type Sequence, } from '@shared/constants'; -import type { FrameworkConfig } from '../programs/framework-config'; +import type { FrameworkConfig } from '@programs/types'; import type { WizardReadinessResult } from '@shared/health-checks/readiness'; import type { SettingsConflict } from '@shared/claude-settings'; import type { ApiUser, ApiProject, Credentials } from '@shared/api'; diff --git a/src/lib/wizard-spellbook.ts b/src/lib/wizard-spellbook.ts index 4ecbd0687..36af8c739 100644 --- a/src/lib/wizard-spellbook.ts +++ b/src/lib/wizard-spellbook.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises'; import path from 'path'; import { getSkillsBaseUrl, POSTHOG_DOCS_URL } from '@shared/constants'; -import type { ProgramConfig } from '../programs/program-step'; +import type { ProgramConfig } from '@programs/types'; import type { WizardSession } from './wizard-session'; import { downloadSkill } from '@agent'; import { From 7068bd342f338d59ced91ea38ffe4e234dee8d79 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 22 Sep 2026 23:01:38 -0400 Subject: [PATCH 38/61] style(agent): format orchestrator error import --- .../runner/sequence/orchestrator/orchestrator-runner.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 4f3c4513c..b0906ce89 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -31,11 +31,7 @@ import { ciExcludedTaskTypes } from '@utils/ci-flag-overrides'; import { logToFile } from '@utils/debug'; import { ringTerminalBell } from '@utils/terminal-bell'; import { AGENT_ERROR_CODE } from '@agent/error-map'; -import { - classifyRunFailure, - ErrorCodes, - WizardError, -} from '@shared/errors'; +import { classifyRunFailure, ErrorCodes, WizardError } from '@shared/errors'; import type { AgentResult } from '../../harness/types'; import type { AgentInteraction } from '@agent/progress'; import type { From 690bdc07bbf8a9afcaf9df037e48a5e274343eaf Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 10:49:51 -0400 Subject: [PATCH 39/61] fix(agent): give each ask its own cancel signal and leave analytics shutdown to the host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AgentInteraction.ask` and `taskNotice` now take `{ signal }`, as §4.3 of the plan specifies. The ask bridge and the seeded-task offer each own one AbortController per request, and a timeout aborts only that request. `cancelAsk` and `cancelTaskNotice` are gone. The host adapter dismisses the overlay when the signal aborts, catches and logs anything the dismissal throws, and detaches once the request settles. The agent no longer calls `analytics.shutdown('success')`. The legacy adapter, the only `runAgent` caller at this layer, sends it after a non-composed run succeeds. Failure and abort still shut down through wizardAbort. A long-lived host can now decide when terminal analytics go out, instead of the first run deciding for it. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/lib/__tests__/wizard-ask-bridge.test.ts | 55 +++++++-- .../__tests__/run-agent-standalone.test.ts | 22 ++-- src/lib/agent/progress.ts | 17 ++- .../__tests__/pending-question.test.ts | 8 +- src/lib/agent/runner/index.ts | 3 +- src/lib/agent/runner/sequence/linear.ts | 1 - .../__tests__/task-notice-timeout.test.ts | 32 ++--- .../orchestrator/orchestrator-runner.ts | 15 ++- src/lib/agent/runner/shared/ask.ts | 7 +- .../__tests__/run-agent-legacy.test.ts | 65 +++++++--- src/lib/programs/run-agent-legacy.ts | 8 +- src/lib/wizard-ask-bridge.ts | 36 +++--- src/ui/__tests__/agent-progress.test.ts | 114 +++++++++++++++--- src/ui/agent-progress.ts | 34 +++++- 14 files changed, 313 insertions(+), 104 deletions(-) diff --git a/src/lib/__tests__/wizard-ask-bridge.test.ts b/src/lib/__tests__/wizard-ask-bridge.test.ts index 1f12b9a4c..650f71b81 100644 --- a/src/lib/__tests__/wizard-ask-bridge.test.ts +++ b/src/lib/__tests__/wizard-ask-bridge.test.ts @@ -212,15 +212,17 @@ describe('createWizardAskBridge', () => { }); describe('timeout', () => { - it('resolves every field with the cancelled sentinel and dismisses the host overlay when the user does not answer in time', async () => { + it('resolves every field with the cancelled sentinel and aborts the question when the user does not answer in time', async () => { vi.useFakeTimers(); try { // showQuestion intentionally never resolves — the timeout has to win. - const cancelQuestion = vi.fn(); + const signals: AbortSignal[] = []; const bridge = createWizardAskBridge({ getSource: () => 'product-tours', - showQuestion: () => new Promise(() => undefined), - cancelQuestion, + showQuestion: (_question, { signal }) => { + signals.push(signal); + return new Promise(() => undefined); + }, timeoutMs: 1000, }); @@ -230,6 +232,7 @@ describe('createWizardAskBridge', () => { { id: 'audience', prompt: 'Who?', kind: 'text' }, ], }); + expect(signals[0].aborted).toBe(false); vi.advanceTimersByTime(1000); @@ -244,7 +247,7 @@ describe('createWizardAskBridge', () => { // Without this, the host's pending-question state survives the // timeout and every later wizard_ask in the run is rejected as a // duplicate request. - expect(cancelQuestion).toHaveBeenCalledTimes(1); + expect(signals[0].aborted).toBe(true); const cancelledCall = wizardCaptureMock.mock.calls.find( ([name]) => name === 'wizard_ask cancelled', @@ -255,14 +258,16 @@ describe('createWizardAskBridge', () => { } }); - it('does not dismiss the overlay when the user answers before the timeout', async () => { + it('does not abort the question when the user answers before the timeout', async () => { vi.useFakeTimers(); try { - const cancelQuestion = vi.fn(); + const signals: AbortSignal[] = []; const bridge = createWizardAskBridge({ getSource: () => 'product-tours', - showQuestion: () => Promise.resolve({ goal: 'ship it' }), - cancelQuestion, + showQuestion: (_question, { signal }) => { + signals.push(signal); + return Promise.resolve({ goal: 'ship it' }); + }, timeoutMs: 1000, }); @@ -271,7 +276,37 @@ describe('createWizardAskBridge', () => { }); vi.advanceTimersByTime(1000); - expect(cancelQuestion).not.toHaveBeenCalled(); + expect(signals[0].aborted).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it('aborts only the question whose timeout fired', async () => { + vi.useFakeTimers(); + try { + const signals: AbortSignal[] = []; + const bridge = createWizardAskBridge({ + getSource: () => 'product-tours', + showQuestion: (_question, { signal }) => { + signals.push(signal); + return new Promise(() => undefined); + }, + timeoutMs: 1000, + }); + const questions = [ + { id: 'goal', prompt: 'Goal?', kind: 'text' as const }, + ]; + + const first = bridge.request({ questions }); + vi.advanceTimersByTime(500); + void bridge.request({ questions }); + vi.advanceTimersByTime(500); + + await expect(first).resolves.toMatchObject({ timedOut: true }); + // The second question is still open: the first one's timeout must not + // dismiss it. + expect(signals.map((signal) => signal.aborted)).toEqual([true, false]); } finally { vi.useRealTimers(); } diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts index 481e8a5eb..3ce26d1c8 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -311,7 +311,7 @@ describe('runAgent standalone', () => { const result = await running; expect(result.outcome).toBe(RunOutcome.Success); expect(result.snapshot.statusMessages).toContain('answered:{"q1":"yes"}'); - expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); + expect(analytics.shutdown).not.toHaveBeenCalled(); expect(flushScanReport).toHaveBeenCalledTimes(1); }, ); @@ -377,8 +377,8 @@ describe('runAgent standalone', () => { ]); } } - expect(analytics.shutdown).toHaveBeenCalledTimes(2); - expect(analytics.shutdown).toHaveBeenCalledWith('success'); + // Terminal analytics belong to the process, so to the host. + expect(analytics.shutdown).not.toHaveBeenCalled(); }, ); @@ -434,7 +434,7 @@ describe('runAgent standalone', () => { }); it.each([Sequence.linear, Sequence.orchestrator])( - 'preserves %s completion, shutdown and scan-flush ordering', + 'preserves %s completion and scan-flush ordering and leaves the shutdown to the host', async (sequence) => { const order: string[] = []; vi.mocked(analytics.shutdown).mockImplementationOnce(() => { @@ -473,7 +473,6 @@ describe('runAgent standalone', () => { 'queue-clean', 'completion', 'outro', - 'shutdown', 'scan-flush', ]); }, @@ -502,11 +501,16 @@ describe('runAgent standalone', () => { }); expect(result.failure).toBeUndefined(); - // The answerer saw the question the bridge built, and its answer came back. + // The answerer saw the question the bridge built, with that question's own + // signal, and its answer came back. expect(ask).toHaveBeenCalledTimes(1); - const [question] = ask.mock.calls[0] as unknown as [PendingQuestion]; + const [question, context] = ask.mock.calls[0] as unknown as [ + PendingQuestion, + { signal: AbortSignal }, + ]; expect(question.questions[0].id).toBe('q1'); expect(question.source).toBe('test-integration'); + expect(context.signal.aborted).toBe(false); expect(events).toContainEqual({ kind: 'status', message: 'answered:{"q1":"yes"}', @@ -534,7 +538,7 @@ describe('runAgent standalone', () => { cacheReadTokens: 0, cacheCreationTokens: 0, }); - expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); + expect(analytics.shutdown).not.toHaveBeenCalled(); }); it('runs to a complete result with no options at all', async () => { @@ -618,7 +622,7 @@ describe('runAgent standalone', () => { expect(result.failure).toBe(failure); }); - it('skips the terminal outro and the shutdown for a composed sub-run', async () => { + it('skips the terminal outro for a composed sub-run', async () => { const events: AgentProgress[] = []; const result = await runAgent(config({ composed: true }), input(), { diff --git a/src/lib/agent/progress.ts b/src/lib/agent/progress.ts index 8be0d52f2..5e246d40d 100644 --- a/src/lib/agent/progress.ts +++ b/src/lib/agent/progress.ts @@ -77,17 +77,22 @@ export type ProgressEmitter = (event: AgentProgress) => void; * capability is optional. With none supplied the agent installs no ask bridge, * so `wizard_ask` returns its existing "not available" error, and an optional * task notice is declined — the same path a `--ci` run takes today. + * On a request's `signal` abort, the host dismisses that request alone, and + * that dismissal must not throw: abort listeners run where the agent cannot + * catch them, so Node would rethrow the error as an uncaught exception. */ export interface AgentInteraction { /** * Open a question and resolve with the answers. The bridge that calls this * owns the timeout, the `__cancelled__` sentinel and the analytics. */ - ask?: (question: PendingQuestion) => Promise; - /** Dismiss the in-flight question as cancelled (timeouts call this). */ - cancelAsk?: () => void; + ask?: ( + question: PendingQuestion, + context: { signal: AbortSignal }, + ) => Promise; /** Offer an optional step and resolve with whether to keep it. */ - taskNotice?: (notice: TaskNotice) => Promise; - /** Dismiss an in-flight task notice as declined (timeouts call this). */ - cancelTaskNotice?: () => void; + taskNotice?: ( + notice: TaskNotice, + context: { signal: AbortSignal }, + ) => Promise; } diff --git a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts b/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts index 4102211ea..966cbcb9d 100644 --- a/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts +++ b/src/lib/agent/runner/harness/anthropic/__tests__/pending-question.test.ts @@ -127,17 +127,17 @@ describe.each(['linear', 'task'] as const)( vi.useFakeTimers(); let resolve!: (answers: AskAnswers) => void; let reject!: (error: Error) => void; - const cancelAsk = vi.fn(); + let signal: AbortSignal | undefined; const bridge = createAskBridge( { - ask: () => { + ask: (_question, context) => { expectPermission('deny'); + signal = context.signal; return new Promise((res, rej) => { resolve = res; reject = rej; }); }, - cancelAsk, }, { getSource: () => 'test', richLinks: false, timeoutMs: 1000 }, ); @@ -164,7 +164,7 @@ describe.each(['linear', 'task'] as const)( } expectPermission('allow'); - expect(cancelAsk).toHaveBeenCalledTimes(ending === 'timeout' ? 1 : 0); + expect(signal?.aborted).toBe(ending === 'timeout'); expect(vi.getTimerCount()).toBe(0); }, ); diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts index 10ea0bdf8..9a0d7c103 100644 --- a/src/lib/agent/runner/index.ts +++ b/src/lib/agent/runner/index.ts @@ -13,7 +13,8 @@ * [skill install] → agent init → prompt → run → errors → [postRun] → outro * * The agent reports and asks, it never renders, never reads a session, never - * exits the process and never rejects. A decided failure comes back in + * exits the process, never sends the process's terminal analytics and never + * rejects. A decided failure comes back in * `RunResult.failure` with the same fields `wizardAbort` takes; an error the * agent did not decide (a refused mint, an SDK crash) comes back as * `outcome: RunOutcome.Crashed` with the original error attached, so a caller can keep diff --git a/src/lib/agent/runner/sequence/linear.ts b/src/lib/agent/runner/sequence/linear.ts index cbdc169dc..c452a4fa8 100644 --- a/src/lib/agent/runner/sequence/linear.ts +++ b/src/lib/agent/runner/sequence/linear.ts @@ -314,6 +314,5 @@ export async function runLinearProgram({ emit({ kind: 'lifecycle', phase: 'completed', message: run.successMessage }); - await analytics.shutdown('success'); return { outcome: RunOutcome.Success, outro: outroData }; } diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts index 79056bd33..c0be82feb 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts @@ -11,13 +11,14 @@ import type { TaskNotice } from '@lib/wizard-session'; // Hoisted: `vi.mock` factories are lifted above the imports, so the analytics // factory would otherwise read these before they exist. -const { showTaskNotice, cancelTaskNotice, wizardCapture, captureException } = - vi.hoisted(() => ({ - showTaskNotice: vi.fn<(notice: TaskNotice) => Promise>(), - cancelTaskNotice: vi.fn(), - wizardCapture: vi.fn(), - captureException: vi.fn(), - })); +const { showTaskNotice, wizardCapture, captureException } = vi.hoisted(() => ({ + showTaskNotice: + vi.fn< + (notice: TaskNotice, context: { signal: AbortSignal }) => Promise + >(), + wizardCapture: vi.fn(), + captureException: vi.fn(), +})); vi.mock('@utils/analytics', () => ({ analytics: { @@ -36,9 +37,13 @@ import { } from '@lib/agent/runner/sequence/orchestrator/orchestrator-runner'; /** The answerer under test, standing where `getUI()` used to. */ -const interaction = { - taskNotice: (notice: TaskNotice) => showTaskNotice(notice), - cancelTaskNotice: () => cancelTaskNotice(), +const interaction = { taskNotice: showTaskNotice }; + +/** The signal the offer handed the host with its one notice. */ +const noticeSignal = (): AbortSignal => { + const call = showTaskNotice.mock.calls[0]; + if (!call) throw new Error('No notice was shown'); + return call[1].signal; }; const NOTICE: TaskNotice = { @@ -52,7 +57,6 @@ const NOTICE: TaskNotice = { const resetMocks = () => { showTaskNotice.mockReset(); - cancelTaskNotice.mockReset(); wizardCapture.mockReset(); captureException.mockReset(); }; @@ -75,7 +79,7 @@ describe('task notice timeout', () => { await expect(promise).resolves.toEqual({ keep: false, timedOut: true }); // Without this the modal stays on screen over the rest of the run. - expect(cancelTaskNotice).toHaveBeenCalledTimes(1); + expect(noticeSignal().aborted).toBe(true); } finally { vi.useRealTimers(); } @@ -95,7 +99,7 @@ describe('task notice timeout', () => { vi.advanceTimersByTime(5000); // The timer must not fire after an answer — it would close a modal that // is no longer there and stamp a second outcome on the step. - expect(cancelTaskNotice).not.toHaveBeenCalled(); + expect(noticeSignal().aborted).toBe(false); } finally { vi.useRealTimers(); } @@ -114,7 +118,7 @@ describe('task notice timeout', () => { keep: false, timedOut: false, }); - expect(cancelTaskNotice).not.toHaveBeenCalled(); + expect(noticeSignal().aborted).toBe(false); } finally { vi.useRealTimers(); } diff --git a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 64d49c67e..c0b2b128a 100644 --- a/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -226,21 +226,25 @@ export async function offerSeededTask( // No one to show the notice to: a step nobody can answer for must not run. // The same answer a non-interactive host gives today. if (!interaction?.taskNotice) return { keep: false, timedOut: false }; - const { taskNotice, cancelTaskNotice } = interaction; + const { taskNotice } = interaction; + const controller = new AbortController(); let timer: ReturnType | undefined; let timedOut = false; const timeout = new Promise((resolve) => { timer = setTimeout(() => { timedOut = true; - // Dismisses the overlay and settles the showTaskNotice promise too, so - // the losing side of the race cannot leave a modal on screen. - cancelTaskNotice?.(); + // The host dismisses this notice's overlay and settles its promise too, + // so the losing side of the race cannot leave a modal on screen. + controller.abort(); resolve(false); }, timeoutMs); }); try { - const keep = await Promise.race([taskNotice(notice), timeout]); + const keep = await Promise.race([ + taskNotice(notice, { signal: controller.signal }), + timeout, + ]); return { keep, timedOut }; } finally { if (timer) clearTimeout(timer); @@ -1298,6 +1302,5 @@ async function executeOrchestrator( }; emit({ kind: 'completion', outro }); emit({ kind: 'lifecycle', phase: 'completed', message }); - await analytics.shutdown('success'); return { outcome: RunOutcome.Success, outro }; } diff --git a/src/lib/agent/runner/shared/ask.ts b/src/lib/agent/runner/shared/ask.ts index b7c6c1050..56cae7874 100644 --- a/src/lib/agent/runner/shared/ask.ts +++ b/src/lib/agent/runner/shared/ask.ts @@ -3,7 +3,7 @@ * * `createWizardAskBridge` already owns request ids, the timeout race, the * `__cancelled__` sentinel and the analytics; it only ever needed a - * `showQuestion` and a `cancelQuestion`. Here those come from + * `showQuestion` that honours each question's own signal. Here that comes from * `AgentInteraction` instead of `getUI()`. With no answerer there is no bridge, * so `wizard_ask` reports its existing "not available" error rather than * hanging on a question nobody can see. @@ -30,11 +30,10 @@ export function createAskBridge( return createWizardAskBridge({ getSource: options.getSource, - showQuestion: (question) => { + showQuestion: (question, context) => { options.beforeShow?.(); - return ask(question); + return ask(question, context); }, - cancelQuestion: interaction?.cancelAsk, richLinks: options.richLinks, timeoutMs: options.timeoutMs, }); diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index 6a0b9b1c4..ab074bcbf 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -106,6 +106,21 @@ const session = () => ({ let logSpy: ReturnType; +/** A run that reports, shows its outro and succeeds. */ +const finishRun: typeof runAgent = (_config, _input, options) => { + options?.onProgress?.({ kind: 'status', message: 'Working' }); + options?.onProgress?.({ + kind: 'completion', + outro: { kind: OutroKind.Success, message: 'Done' }, + }); + options?.onProgress?.({ + kind: 'lifecycle', + phase: 'completed', + message: 'Done', + }); + return Promise.resolve({ outcome: RunOutcome.Success, snapshot }); +}; + beforeEach(() => { vi.clearAllMocks(); vi.mocked(authenticate).mockImplementation((sess) => { @@ -114,19 +129,7 @@ beforeEach(() => { }); setUI(new LoggingUI()); logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); - vi.mocked(runAgent).mockImplementation((_config, _input, options) => { - options?.onProgress?.({ kind: 'status', message: 'Working' }); - options?.onProgress?.({ - kind: 'completion', - outro: { kind: OutroKind.Success, message: 'Done' }, - }); - options?.onProgress?.({ - kind: 'lifecycle', - phase: 'completed', - message: 'Done', - }); - return Promise.resolve({ outcome: RunOutcome.Success, snapshot }); - }); + vi.mocked(runAgent).mockImplementation(finishRun); }); afterEach(() => logSpy.mockRestore()); @@ -151,10 +154,35 @@ it.each([ expect(logSpy).toHaveBeenCalledWith('◇ Working'); expect(logSpy).toHaveBeenCalledWith('└ Done'); expect(initLogFile).toHaveBeenCalledOnce(); - expect(analytics.shutdown).not.toHaveBeenCalled(); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); }, ); +it('sends terminal analytics after the outro and the run, before the host goes on', async () => { + const order: string[] = []; + logSpy.mockImplementation((line) => { + if (line === '└ Done') order.push('outro'); + }); + vi.mocked(runAgent).mockImplementation(async (...args) => { + const result = await finishRun(...args); + order.push('run-returned'); + return result; + }); + vi.mocked(analytics.shutdown).mockImplementation(() => { + order.push('shutdown'); + return Promise.resolve(); + }); + await runProgramAgent(program(), session()); + order.push('host-continues'); + expect(order).toEqual([ + 'outro', + 'run-returned', + 'shutdown', + 'host-continues', + ]); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); +}); + it('clamps a composed program to linear and keeps host analytics alive', async () => { await runProgramAgent(program(), session(), { composed: true }); expect(runAgent).toHaveBeenCalledWith( @@ -166,6 +194,10 @@ it('clamps a composed program to linear and keeps host analytics alive', async ( expect.anything(), ); expect(analytics.shutdown).not.toHaveBeenCalled(); + + // The host program's own run, later in the same process, ends it once. + await runProgramAgent(program(), session()); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); }); it.each([RunOutcome.Aborted, RunOutcome.Failed] as const)( @@ -214,6 +246,11 @@ it.each([ ); await vi.waitFor(() => expect(streamShutdown).toHaveBeenCalledOnce()); expect(wizardAbort).not.toHaveBeenCalled(); + // One terminal event for the process, sent before the stream settles. + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); + expect( + vi.mocked(analytics.shutdown).mock.invocationCallOrder[0], + ).toBeLessThan(streamShutdown.mock.invocationCallOrder[0]); expect(runAgent).toHaveBeenCalledWith( expect.objectContaining({ binding: expect.objectContaining({ harness, sequence }), diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index d8315cb88..a22ced039 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -7,7 +7,8 @@ * authenticates, resolves the program's binding, builds the agent's inputs * from the session, maps every progress event back onto `getUI()` one call * per event, answers the agent's questions through `getUI()`, and applies the - * result — `wizardAbort` for a decided failure, nothing more for success. + * result — `wizardAbort` for a decided failure, the terminal analytics event + * for a finished top-level run. * * This is the only file that knows about `getUI()`, the session and * `wizardAbort` on the agent's behalf. Programs replace it in Release B. @@ -275,12 +276,15 @@ async function runProgram( interaction: uiInteraction(ui), }); - // The host owns process exits and rethrowing crashes. + // The host owns process exits, terminal analytics and rethrowing crashes. if (result.outcome === RunOutcome.Crashed) { throw result.failure.error; } if (result.outcome !== RunOutcome.Success) { await wizardAbort(result.failure); + } else if (!composed) { + // A composed sub-run leaves the terminal event to its host program's run. + await analytics.shutdown('success'); } } diff --git a/src/lib/wizard-ask-bridge.ts b/src/lib/wizard-ask-bridge.ts index 9dc974d60..1a007a8a8 100644 --- a/src/lib/wizard-ask-bridge.ts +++ b/src/lib/wizard-ask-bridge.ts @@ -60,8 +60,20 @@ export interface WizardAskBridge { export interface WizardAskBridgeOptions { /** Returns the active skill id, used as the analytics `source` on the request. */ getSource: () => string; - /** Opens the overlay and resolves once the user submits or cancels. */ - showQuestion: (question: PendingQuestion) => Promise; + /** + * Opens the overlay and resolves once the user submits or cancels. `signal` + * is this question's own: it aborts when the timeout wins the race, and the + * host dismisses this question's overlay. Without that the host keeps its + * pending-question state, and every later `wizard_ask` in the run fails with + * "another request is pending" — one unanswered prompt would block + * credential collection for all remaining sources. The host's abort + * handling must not throw: the bridge cannot catch an abort listener's + * error, and Node rethrows it as an uncaught exception. + */ + showQuestion: ( + question: PendingQuestion, + context: { signal: AbortSignal }, + ) => Promise; /** * Per-question timeout in milliseconds. When the user takes longer than * this to answer, every unanswered field resolves with the @@ -74,14 +86,6 @@ export interface WizardAskBridgeOptions { * Propagated onto every {@link PendingQuestion} this bridge creates. */ richLinks?: boolean; - /** - * Dismiss the host's in-flight question overlay. Called when the timeout - * wins the race: without it the host keeps its pending-question state, and - * every later `wizard_ask` in the run fails with "another request is - * pending" — one unanswered prompt would block credential collection for - * all remaining sources. - */ - cancelQuestion?: () => void; } /** Sentinel returned for unanswered fields on cancellation or timeout. */ @@ -134,24 +138,26 @@ export function createWizardAskBridge( pendingQuestions.set(pending.id, pending); const startedAt = Date.now(); + const controller = new AbortController(); let timer: ReturnType | undefined; let timedOut = false; // Race the user against the timeout. Whichever fires first wins. On - // timeout we also cancel the host's overlay: resolving our side alone - // would leave the host's pending-question state set, and the next - // wizard_ask would be rejected as a duplicate request. + // timeout we also abort this question's signal so the host dismisses its + // overlay: resolving our side alone would leave the host's + // pending-question state set, and the next wizard_ask would be rejected + // as a duplicate request. const timeoutPromise = new Promise((resolve) => { timer = setTimeout(() => { timedOut = true; - opts.cancelQuestion?.(); + controller.abort(); resolve(buildCancelledAnswers(questions)); }, timeoutMs); }); try { const answers = await Promise.race([ - opts.showQuestion(pending), + opts.showQuestion(pending, { signal: controller.signal }), timeoutPromise, ]); const durationMs = Date.now() - startedAt; diff --git a/src/ui/__tests__/agent-progress.test.ts b/src/ui/__tests__/agent-progress.test.ts index b6bf8c49e..bdb1535fd 100644 --- a/src/ui/__tests__/agent-progress.test.ts +++ b/src/ui/__tests__/agent-progress.test.ts @@ -1,10 +1,20 @@ vi.mock('@ui', () => ({ getUI: vi.fn() })); vi.mock('@utils/debug'); +vi.mock('@utils/analytics', () => ({ analytics: { wizardCapture: vi.fn() } })); import { createUiReducer, uiInteraction } from '../agent-progress'; import { LoggingUI } from '../logging-ui'; import type { AgentProgress } from '@lib/agent/progress'; +import { + CANCELLED_SENTINEL, + createWizardAskBridge, +} from '@lib/wizard-ask-bridge'; import { OutroKind } from '@lib/wizard-session'; +import { logToFile } from '@utils/debug'; + +beforeEach(() => { + vi.mocked(logToFile).mockClear(); +}); it('projects every progress event onto the matching UI call, in order', () => { const ui = new LoggingUI(); @@ -102,28 +112,104 @@ it('projects every progress event onto the matching UI call, in order', () => { ]); }); -it('forwards answers, notices and their dismissals', async () => { +const question = { id: 'q', source: 'test', questions: [] }; +const notice = { + title: 'Optional', + body: [], + items: [], + prompt: 'Continue?', + confirmLabel: 'Yes', + cancelLabel: 'No', +}; + +it('forwards answers and notices, leaving the host alone once they settle', async () => { const ui = new LoggingUI(); - const question = { id: 'q', source: 'test', questions: [] }; - const notice = { - title: 'Optional', - body: [], - items: [], - prompt: 'Continue?', - confirmLabel: 'Yes', - cancelLabel: 'No', - }; const ask = vi.spyOn(ui, 'requestQuestion').mockResolvedValue({ q: 'yes' }); const cancelAsk = vi.spyOn(ui, 'cancelPendingQuestion'); const show = vi.spyOn(ui, 'showTaskNotice').mockResolvedValue(true); const cancelNotice = vi.spyOn(ui, 'cancelTaskNotice'); const interaction = uiInteraction(ui); - await expect(interaction.ask?.(question)).resolves.toEqual({ q: 'yes' }); - await expect(interaction.taskNotice?.(notice)).resolves.toBe(true); - interaction.cancelAsk?.(); - interaction.cancelTaskNotice?.(); + const asked = new AbortController(); + const noticed = new AbortController(); + await expect( + interaction.ask?.(question, { signal: asked.signal }), + ).resolves.toEqual({ q: 'yes' }); + await expect( + interaction.taskNotice?.(notice, { signal: noticed.signal }), + ).resolves.toBe(true); + // A late abort must not dismiss whatever the host shows next. + asked.abort(); + noticed.abort(); expect(ask).toHaveBeenCalledWith(question); expect(show).toHaveBeenCalledWith(notice); + expect(cancelAsk).not.toHaveBeenCalled(); + expect(cancelNotice).not.toHaveBeenCalled(); +}); + +it('dismisses an open question or notice when its signal aborts', () => { + const ui = new LoggingUI(); + vi.spyOn(ui, 'requestQuestion').mockReturnValue(new Promise(() => undefined)); + const cancelAsk = vi.spyOn(ui, 'cancelPendingQuestion'); + vi.spyOn(ui, 'showTaskNotice').mockReturnValue(new Promise(() => undefined)); + const cancelNotice = vi.spyOn(ui, 'cancelTaskNotice'); + const interaction = uiInteraction(ui); + const asked = new AbortController(); + const noticed = new AbortController(); + void interaction.ask?.(question, { signal: asked.signal }); + void interaction.taskNotice?.(notice, { signal: noticed.signal }); + + asked.abort(); expect(cancelAsk).toHaveBeenCalledOnce(); + expect(cancelNotice).not.toHaveBeenCalled(); + noticed.abort(); expect(cancelNotice).toHaveBeenCalledOnce(); }); + +it('settles a timed-out question when the host dismissal throws', async () => { + vi.useFakeTimers(); + try { + const ui = new LoggingUI(); + vi.spyOn(ui, 'requestQuestion').mockReturnValue( + new Promise(() => undefined), + ); + const broken = new Error('overlay broken'); + vi.spyOn(ui, 'cancelPendingQuestion').mockImplementation(() => { + throw broken; + }); + const { ask } = uiInteraction(ui); + if (!ask) throw new Error('uiInteraction answers questions'); + const bridge = createWizardAskBridge({ + getSource: () => 'test', + showQuestion: ask, + timeoutMs: 1000, + }); + const result = bridge.request({ + questions: [{ id: 'goal', prompt: 'Goal?', kind: 'text' }], + }); + vi.advanceTimersByTime(1000); + await expect(result).resolves.toEqual({ + answers: { goal: CANCELLED_SENTINEL }, + timedOut: true, + }); + expect(bridge.getPendingQuestion()).toBeNull(); + // Node rethrows an abort listener's error as an uncaught exception the + // bridge cannot catch, so the answerer logs it instead. + expect(logToFile).toHaveBeenCalledWith(expect.any(String), broken); + } finally { + vi.useRealTimers(); + } +}); + +it('logs a throwing notice dismissal instead of throwing from the abort', () => { + const ui = new LoggingUI(); + vi.spyOn(ui, 'showTaskNotice').mockReturnValue(new Promise(() => undefined)); + const broken = new Error('overlay broken'); + vi.spyOn(ui, 'cancelTaskNotice').mockImplementation(() => { + throw broken; + }); + const noticed = new AbortController(); + void uiInteraction(ui).taskNotice?.(notice, { signal: noticed.signal }); + + noticed.abort(); + expect(logToFile).toHaveBeenCalledWith(expect.any(String), broken); +}); diff --git a/src/ui/agent-progress.ts b/src/ui/agent-progress.ts index d3e3450cc..c92f1c108 100644 --- a/src/ui/agent-progress.ts +++ b/src/ui/agent-progress.ts @@ -1,5 +1,6 @@ import type { WizardUI, SpinnerHandle } from './wizard-ui'; import type { AgentInteraction, AgentProgress } from '@lib/agent/progress'; +import { logToFile } from '@utils/debug'; // ── Progress → WizardUI, one call per event ─────────────────────────── @@ -63,9 +64,34 @@ export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { /** The agent's questions, answered wherever `getUI()` answers them today. */ export function uiInteraction(ui: WizardUI): AgentInteraction { return { - ask: (question) => ui.requestQuestion(question), - cancelAsk: () => ui.cancelPendingQuestion(), - taskNotice: (notice) => ui.showTaskNotice(notice), - cancelTaskNotice: () => ui.cancelTaskNotice(), + ask: (question, { signal }) => + dismissOnAbort(ui.requestQuestion(question), signal, () => + ui.cancelPendingQuestion(), + ), + taskNotice: (notice, { signal }) => + dismissOnAbort(ui.showTaskNotice(notice), signal, () => + ui.cancelTaskNotice(), + ), }; } + +/** + * Dismiss one open request on abort; a settled one leaves the UI alone. A + * throw inside an abort listener reaches no caller: Node rethrows it as an + * uncaught exception, so a broken overlay is logged here instead. + */ +function dismissOnAbort( + open: Promise, + signal: AbortSignal, + dismiss: () => void, +): Promise { + const onAbort = () => { + try { + dismiss(); + } catch (error) { + logToFile('[agent-progress] dismissing an aborted request failed', error); + } + }; + signal.addEventListener('abort', onAbort, { once: true }); + return open.finally(() => signal.removeEventListener('abort', onAbort)); +} From c84b5de88a16ff723dcc167e2fc4eb775fe72f50 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 11:58:39 -0400 Subject: [PATCH 40/61] fix(programs): keep a successful run successful when its analytics flush fails After a non-composed success the legacy adapter awaits analytics.shutdown('success'). posthog-core can reject that on its flush timeout, and the rejection turned a finished run into a TUI error outro and exit 1, or into a headless wizardAbort. The adapter now logs the failure and moves on. There is still exactly one terminal shutdown, because the first status latches. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__tests__/run-agent-legacy.test.ts | 64 ++++++++++++++++++- src/lib/programs/run-agent-legacy.ts | 7 +- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index ab074bcbf..f96618232 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -1,4 +1,5 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; +import { runWizard } from '@lib/runners/run-wizard'; import { authenticate } from '@lib/agent/runner/shared/authenticate'; import { runProgramAgent } from '../run-agent-legacy'; import { runAgent, RunOutcome, type RunResult } from '@lib/agent/runner'; @@ -6,9 +7,12 @@ import { Harness, Sequence } from '@lib/constants'; import { buildSession, OutroKind } from '@lib/wizard-session'; import { HostResolution } from '@lib/host-resolution'; import { LoggingUI } from '@ui/logging-ui'; +import { InkUI } from '@ui/tui/ink-ui'; +import { startTUI } from '@ui/tui/start-tui'; +import { WizardStore } from '@ui/tui/store'; import { setUI } from '@ui'; import { analytics } from '@utils/analytics'; -import { initLogFile } from '@utils/debug'; +import { initLogFile, logToFile } from '@utils/debug'; import { wizardAbort } from '@utils/wizard-abort'; import type { ProgramConfig } from '../program-step'; @@ -37,12 +41,14 @@ vi.mock('@lib/task-stream/index', () => ({ PostHogDestination: class {}, createFileDestination: () => null, })); +vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); vi.mock('@utils/debug'); vi.mock('@utils/analytics', () => ({ analytics: { build: 'test', runId: 'run-1', wizardCapture: vi.fn(), + captureException: vi.fn(), setTag: vi.fn(), getAllFlagsForWizard: vi.fn().mockResolvedValue({}), getWizardFlagPayloads: vi.fn().mockReturnValue({}), @@ -262,3 +268,59 @@ it.each([ expect(logSpy).toHaveBeenCalledWith('└ Done'); }, ); + +it('keeps a headless run a success when its terminal analytics flush fails', async () => { + const flushError = new Error('flush timed out'); + vi.mocked(analytics.shutdown).mockRejectedValueOnce(flushError); + runNonInteractive( + program(), + { + apiKey: 'phx_test', + projectId: '1', + installDir: '/tmp/adapter-test', + telemetry: false, + }, + 'headless', + ); + await vi.waitFor(() => expect(streamShutdown).toHaveBeenCalledOnce()); + expect(wizardAbort).not.toHaveBeenCalled(); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); + expect(logToFile).toHaveBeenCalledWith( + expect.stringContaining('analytics shutdown failed'), + flushError, + ); +}); + +it('keeps a TUI run a success when its terminal analytics flush fails', async () => { + const flushError = new Error('flush timed out'); + vi.mocked(analytics.shutdown).mockRejectedValueOnce(flushError); + const store = new WizardStore('metrics'); + const ui = new InkUI(store); + setUI(ui); + const outroError = vi.spyOn(ui, 'outroError'); + vi.spyOn(store, 'runReadyHooks').mockResolvedValue(undefined); + vi.spyOn(store, 'getGate').mockResolvedValue(undefined); + vi.mocked(startTUI).mockReturnValue({ + store, + unmount: vi.fn(), + waitForSetup: () => Promise.resolve(), + }); + const exit = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + + runWizard(program(), { installDir: '/tmp/adapter-test', telemetry: false }); + await vi.waitFor(() => expect(analytics.shutdown).toHaveBeenCalled()); + store.setSkillsComplete(true); + await vi.waitFor(() => expect(exit).toHaveBeenCalled()); + + expect(exit).toHaveBeenCalledExactlyOnceWith(0); + expect(outroError).not.toHaveBeenCalled(); + expect(store.session.outroData?.kind).toBe(OutroKind.Success); + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); + expect(logToFile).toHaveBeenCalledWith( + expect.stringContaining('analytics shutdown failed'), + flushError, + ); + exit.mockRestore(); +}); diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index a22ced039..f813e8d2c 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -284,7 +284,12 @@ async function runProgram( await wizardAbort(result.failure); } else if (!composed) { // A composed sub-run leaves the terminal event to its host program's run. - await analytics.shutdown('success'); + // The run already succeeded: a failed flush is logged, never the outcome. + try { + await analytics.shutdown('success'); + } catch (error) { + logToFile('[agent-runner] analytics shutdown failed:', error); + } } } From 62c4387cbd0d4f89b7956d557d7a146f619bcfa6 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 12:01:50 -0400 Subject: [PATCH 41/61] fix(agent): label decided failures as errors and cancel open asks when a run ends Terminal analytics. Since A3's tagged results, a decided failure with no Error object (MCP missing, YARA violation, no progress, API errors read from output, an SDK result failure, the agent's own [ABORT]) reached wizardAbort without an Error. So the run was labelled 'cancelled' and skipped error tracking. wizardAbort now takes an explicit status. The legacy host passes 'cancelled' only for a host-cancelled run and 'error' for everything else, and captures a WizardError built from the failure's code and message. The agent's own [ABORT] returns Failed; Aborted now means only that the host's signal cancelled the run. What the user sees is unchanged. Orchestrator funnel. When a sibling's fatal result ended the drain, the accepted steps it stopped sent no 'orchestrator task blocked' event. The fatal path now sends them too, through one helper that catches analytics errors, so a failed capture can't turn a decided result into Crashed. Linear cancellation. With no siblings, an ask left open when the harness ended terminally stayed pending until its own timeout. The linear sequence now owns a run controller, as the orchestrator does: the host signal forwards to it, and it aborts when the run ends. The runner and agent READMEs say terminal analytics are the host's. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/__tests__/wizard-abort.test.ts | 29 +++++ src/agent/README.md | 7 +- .../__tests__/run-agent-standalone.test.ts | 118 +++++++++++++++++- src/agent/runner/README.md | 6 +- src/agent/runner/sequence/linear.ts | 36 ++++-- .../runner/sequence/orchestrator/executor.ts | 2 + .../orchestrator/orchestrator-runner.ts | 68 ++++++---- src/agent/wizard-ask-bridge.ts | 1 - .../__tests__/run-agent-legacy.test.ts | 63 +++++++++- src/lib/programs/run-agent-legacy.ts | 10 +- src/shared/utils/wizard-abort.ts | 19 ++- 11 files changed, 307 insertions(+), 52 deletions(-) diff --git a/src/__tests__/wizard-abort.test.ts b/src/__tests__/wizard-abort.test.ts index 66111f8b6..923a53ac6 100644 --- a/src/__tests__/wizard-abort.test.ts +++ b/src/__tests__/wizard-abort.test.ts @@ -187,6 +187,35 @@ describe('wizardAbort', () => { expect(process.exit).toHaveBeenCalledWith(1); }); + it('captures an "error" ending that has no Error from its code and message', async () => { + await expect( + wizardAbort({ + message: 'Could not access MCP', + code: ErrorCodes.AgentMcpMissing, + status: 'error', + }), + ).rejects.toThrow('process.exit called'); + + const [captured, properties] = mockAnalytics.captureException.mock + .calls[0] as [WizardError, Record]; + expect(captured).toBeInstanceOf(WizardError); + expect(captured.message).toBe('Could not access MCP'); + expect(captured.code).toBe(ErrorCodes.AgentMcpMissing); + expect(properties).toEqual({ error_code: ErrorCodes.AgentMcpMissing }); + expect(mockAnalytics.shutdown).toHaveBeenCalledWith('error'); + }); + + it('shuts down as the explicit status even when an Error is provided', async () => { + const error = new Error('stopped'); + + await expect(wizardAbort({ error, status: 'cancelled' })).rejects.toThrow( + 'process.exit called', + ); + + expect(mockAnalytics.captureException).toHaveBeenCalledWith(error, {}); + expect(mockAnalytics.shutdown).toHaveBeenCalledWith('cancelled'); + }); + it('shuts down analytics as "cancelled" when no error is provided', async () => { await expect(wizardAbort({ message: 'Bad input' })).rejects.toThrow( 'process.exit called', diff --git a/src/agent/README.md b/src/agent/README.md index 4998ea053..f73939952 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -47,7 +47,12 @@ runAgent(config: RunConfig, input: RunInput, options?: { - Errors: the agent does not exit the process and returns decided failures. A caught coded error becomes `Failed`. An uncoded throw becomes `Crashed` with the error attached. A gateway 401 returns an auth failure. The host decides - whether to show auth UI. + whether to show auth UI. `Aborted` means the host's signal cancelled the run; + an agent that stops itself with `[ABORT]` returns `Failed` with its abort + code. +- Analytics shutdown is host-owned: the agent never sends the terminal + `setup wizard finished` event. The host sends it from the outcome: `Success` + is `success`, `Aborted` is `cancelled`, `Failed` and `Crashed` are `error`. Other runtime exports: `resolveBinding`, `shouldDisableAsk`, `initializeAgent`, `executeAgent`, `buildRunTags`, `AgentSignals`, diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index 20fe551ca..82e649679 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -18,7 +18,7 @@ import { } from '@lib/wizard-session'; import { AGENT_ERROR_CODE, ErrorCodes, WizardError } from '@shared/errors'; import { AgentErrorType } from '@agent/signals'; -import { CANCELLED_SENTINEL } from '@agent/wizard-ask-bridge'; +import { CANCELLED_SENTINEL, type AskResponse } from '@agent/wizard-ask-bridge'; import type { AgentFailure } from '@agent/runner/shared/types'; import type { AgentProgress } from '@agent/progress'; import type { @@ -81,6 +81,10 @@ const harnessState = vi.hoisted(() => ({ task: undefined as | ((inputs: TaskRunInputs) => Promise) | undefined, + /** Scripts the linear run in place of the default one. */ + run: undefined as + | ((inputs: BackendRunInputs) => Promise) + | undefined, })); vi.mock('@agent/runner/switchboard/harness', () => { const askIfRequested = async (inputs: BackendRunInputs | TaskRunInputs) => { @@ -121,6 +125,7 @@ vi.mock('@agent/runner/switchboard/harness', () => { }, async run(inputs: BackendRunInputs) { harnessState.lastInputs = inputs; + if (harnessState.run) return harnessState.run(inputs); const { emit, spinner } = inputs; emit({ kind: 'log', level: 'step', message: 'Initializing agent' }); spinner.start('Working'); @@ -268,7 +273,9 @@ beforeEach(() => { harnessState.askQuestions = undefined; harnessState.seedTasks = 1; harnessState.task = undefined; + harnessState.run = undefined; vi.mocked(analytics.shutdown).mockClear(); + vi.mocked(analytics.wizardCapture).mockClear(); vi.mocked(initLogFile).mockClear(); vi.mocked(flushScanReport).mockClear(); }); @@ -542,6 +549,112 @@ describe('runAgent standalone', () => { expect(answers).toEqual([{ q1: CANCELLED_SENTINEL }]); }); + it('reports the steps a fatal task result stopped', async () => { + const failure = { + code: ErrorCodes.AgentApiError, + message: 'Gateway rejected the request', + }; + harnessState.task = ({ orchestrator }) => { + if (!orchestrator.currentTaskId) + throw new Error('a drained task has an id'); + // A step that waits on this one, so the fatal result leaves it pending. + orchestrator.store.enqueue({ + type: 'report', + dependsOn: [orchestrator.currentTaskId], + }); + return Promise.resolve({ kind: 'decided_failure', failure }); + }; + const result = await runAgent( + config({ + binding: { + harness: Harness.pi, + sequence: Sequence.orchestrator, + model: DEFAULT_AGENT_MODEL, + }, + }), + input(), + ); + expect(result.outcome).toBe(RunOutcome.Failed); + expect(result.failure).toBe(failure); + expect(analytics.wizardCapture).toHaveBeenCalledWith( + 'orchestrator task blocked', + { type: 'report', optional: false, failed_types: 'install' }, + ); + }); + + it('keeps a fatal task result when reporting its blocked steps throws', async () => { + const failure = { + code: ErrorCodes.AgentApiError, + message: 'Gateway rejected the request', + }; + harnessState.task = ({ orchestrator }) => { + if (!orchestrator.currentTaskId) + throw new Error('a drained task has an id'); + orchestrator.store.enqueue({ + type: 'report', + dependsOn: [orchestrator.currentTaskId], + }); + return Promise.resolve({ kind: 'decided_failure', failure }); + }; + vi.mocked(analytics.wizardCapture).mockImplementation((event) => { + if (event === 'orchestrator task blocked') { + throw new Error('analytics down'); + } + }); + try { + const result = await runAgent( + config({ + binding: { + harness: Harness.pi, + sequence: Sequence.orchestrator, + model: DEFAULT_AGENT_MODEL, + }, + }), + input(), + ); + expect(result.outcome).toBe(RunOutcome.Failed); + expect(result.failure).toBe(failure); + } finally { + vi.mocked(analytics.wizardCapture).mockReset(); + } + }); + + it('cancels an open question when the linear harness ends the run', async () => { + const signals: AbortSignal[] = []; + let response: Promise | undefined; + harnessState.run = (inputs) => { + // A parallel tool call is still waiting on the user when a violation + // ends the run. + if (!inputs.askBridge) throw new Error('the linear run can ask'); + response = inputs.askBridge.request({ + questions: [{ id: 'q1', prompt: 'Key?', kind: 'text' }], + }); + return Promise.resolve({ + kind: 'failure', + classification: AgentErrorType.YARA_VIOLATION, + }); + }; + const result = await runAgent(config(), input(), { + interaction: { + // Nobody answers: only the run ending can close this question. + ask: (_question, { signal }) => { + signals.push(signal); + return new Promise(() => undefined); + }, + }, + }); + expect(result.outcome).toBe(RunOutcome.Failed); + expect(result.failure?.code).toBe( + AGENT_ERROR_CODE[AgentErrorType.YARA_VIOLATION], + ); + expect(signals).toHaveLength(1); + expect(signals[0].aborted).toBe(true); + await expect(response).resolves.toEqual({ + answers: { q1: CANCELLED_SENTINEL }, + timedOut: false, + }); + }); + it.each([Sequence.linear, Sequence.orchestrator])( 'preserves %s completion and scan-flush ordering and leaves the shutdown to the host', async (sequence) => { @@ -696,7 +809,8 @@ describe('runAgent standalone', () => { onProgress: (e) => events.push(e), }); - expect(result.outcome).toBe('aborted'); + // The agent stopped itself; only the host's signal makes a run aborted. + expect(result.outcome).toBe(RunOutcome.Failed); expect(result.failure?.code).toBe(ErrorCodes.AgentAbort); expect(result.failure?.outroData).toMatchObject({ kind: OutroKind.Error, diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index 7279abc53..8e8cc4388 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -143,5 +143,7 @@ the host to present. 4. Harness drives each conversation through its SDK, using the bound model, on the PostHog LLM gateway. 5. The scan report flushes; `runAgent` returns a `RunResult`. -6. The caller applies it: a decided failure goes to `wizardAbort`, a crash is - rethrown for the runner's own handling. +6. The caller applies it: a decided failure goes to `wizardAbort` with the + terminal status its outcome names, a crash is rethrown for the runner's own + handling, and a non-composed success sends the terminal success analytics. + The agent sends no terminal analytics. diff --git a/src/agent/runner/sequence/linear.ts b/src/agent/runner/sequence/linear.ts index 8d9c68af7..a1da4d7fd 100644 --- a/src/agent/runner/sequence/linear.ts +++ b/src/agent/runner/sequence/linear.ts @@ -27,14 +27,27 @@ import { createEmitSpinner } from '../shared/progress-collector'; import { createAskBridge } from '../shared/ask'; import { getHarness } from '../switchboard'; -export async function runLinearProgram({ - config, - input, - boot, - emit, - interaction, - signal, -}: SequenceContext): Promise { +export async function runLinearProgram( + context: SequenceContext, +): Promise { + // Aborts on the host's signal or when the run ends, so no ask outlives it. + const controller = new AbortController(); + const abortFromHost = () => controller.abort(); + context.signal?.addEventListener('abort', abortFromHost, { once: true }); + if (context.signal?.aborted) abortFromHost(); + try { + return await executeLinear(context, controller.signal); + } finally { + context.signal?.removeEventListener('abort', abortFromHost); + controller.abort(); + } +} + +/** The host's `signal` decides the outcome; `runSignal` also ends with the run. */ +async function executeLinear( + { config, input, boot, emit, interaction, signal }: SequenceContext, + runSignal: AbortSignal, +): Promise { const { run, composed } = config; const { skillsBaseUrl, credentials, project } = boot; const { projectApiKey, host, projectId } = credentials; @@ -79,7 +92,7 @@ export async function runLinearProgram({ getSource: () => input.skillId ?? run.integrationLabel, richLinks: run.richLinks ?? false, timeoutMs: run.askTimeoutMs, - signal, + signal: runSignal, }); const middleware = input.flags.benchmark @@ -121,7 +134,7 @@ export async function runLinearProgram({ middleware, model, thinkingLevel, - signal, + signal: runSignal, }); if (signal?.aborted && agentResult.kind === 'success') return aborted(); @@ -164,7 +177,8 @@ export async function runLinearProgram({ matched: matched?.message ?? null, }); return { - outcome: RunOutcome.Aborted, + // An agent that stops itself failed the run; only the host's signal cancels it. + outcome: signal?.aborted ? RunOutcome.Aborted : RunOutcome.Failed, failure: { message: matched?.message ?? `${run.integrationLabel} aborted`, outroData, diff --git a/src/agent/runner/sequence/orchestrator/executor.ts b/src/agent/runner/sequence/orchestrator/executor.ts index f33baebdc..f9cf187d1 100644 --- a/src/agent/runner/sequence/orchestrator/executor.ts +++ b/src/agent/runner/sequence/orchestrator/executor.ts @@ -46,6 +46,8 @@ export class RunTaskFatal extends Error { public readonly outcome: | RunOutcome.Aborted | RunOutcome.Failed = RunOutcome.Failed, + /** The type of the task that ended the run, for the steps it stopped. */ + public readonly taskType?: string, ) { super(failure.message ?? 'agent run failed'); this.name = 'RunTaskFatal'; diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index f44be8aba..147a5d843 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -161,20 +161,18 @@ function requireTaskHarness(pick: HarnessPick): AgentHarness & { }; } -function terminalResult(result: AgentResult): - | { - outcome: RunOutcome.Aborted | RunOutcome.Failed; - failure: AgentFailure; - } - | undefined { +function terminalResult( + result: AgentResult, +): { outcome: RunOutcome.Failed; failure: AgentFailure } | undefined { switch (result.kind) { case 'success': return undefined; case 'decided_failure': return { outcome: RunOutcome.Failed, failure: result.failure }; case 'abort': + // Callers return first on the run's signal, so this abort is the agent's own. return { - outcome: RunOutcome.Aborted, + outcome: RunOutcome.Failed, failure: { code: AGENT_ERROR_CODE[result.classification], message: result.message ?? 'Agent aborted', @@ -301,7 +299,6 @@ export async function offerSeededTask( controller.abort(); }; signal?.addEventListener('abort', cancelForAbort, { once: true }); - if (signal?.aborted) cancelForAbort(); }); try { const keep = await Promise.race([ @@ -486,6 +483,25 @@ export function describeDrainFailure(verdict: { return parts.join(', so '); } +/** One `orchestrator task blocked` event per pending task, best effort once the outcome is decided. */ +function reportBlockedTasks( + tasks: readonly QueuedTask[], + failedTypes: readonly string[], +): void { + for (const task of tasks) { + if (task.status !== TaskStatus.Pending) continue; + try { + analytics.wizardCapture('orchestrator task blocked', { + type: task.type, + optional: task.optional === true, + failed_types: failedTypes.join(',') || 'none', + }); + } catch { + // Reporting must not replace the run result. + } + } +} + /** How many tasks deep in the graph a task sits — 0 when it depends on nothing. */ function graphDepth( task: QueuedTask, @@ -1208,15 +1224,20 @@ async function executeOrchestrator( if (signal?.aborted) return; if (error instanceof RunTaskFatal) throw error; const failure = classifyRunFailure(error); - throw new RunTaskFatal({ - code: failure.code, - message: failure.message, - error: error instanceof Error ? error : undefined, - }); + throw new RunTaskFatal( + { + code: failure.code, + message: failure.message, + error: error instanceof Error ? error : undefined, + }, + RunOutcome.Failed, + task.type, + ); } if (signal?.aborted) return; const terminal = terminalResult(taskResult); - if (terminal) throw new RunTaskFatal(terminal.failure, terminal.outcome); + if (terminal) + throw new RunTaskFatal(terminal.failure, terminal.outcome, task.type); } finally { // Durable skills a task installed are irrelevant to later tasks — and // the sdk harness auto-loads .claude/skills into every agent — so sweep @@ -1300,7 +1321,15 @@ async function executeOrchestrator( } } - if (fatal) return { outcome: fatal.outcome, failure: fatal.failure }; + if (fatal) { + // The steps the fatal task stopped still get their terminal event. + const stoppedBy = drainVerdict(store.list()).requiredFailedTypes; + if (fatal.taskType && !stoppedBy.includes(fatal.taskType)) { + stoppedBy.push(fatal.taskType); + } + reportBlockedTasks(store.list(), stoppedBy); + return { outcome: fatal.outcome, failure: fatal.failure }; + } if (signal?.aborted) return cancelledRun(); renderQueue(); @@ -1345,14 +1374,7 @@ async function executeOrchestrator( // drops out of the funnel. The queue itself is left alone, because the run // cache is already wiped by here and writing to it would recreate the folder // the cleanup just removed. - for (const task of store.list()) { - if (task.status !== TaskStatus.Pending) continue; - analytics.wizardCapture('orchestrator task blocked', { - type: task.type, - optional: task.optional === true, - failed_types: verdict.requiredFailedTypes.join(',') || 'none', - }); - } + reportBlockedTasks(store.list(), verdict.requiredFailedTypes); if (verdict.requiredFailedTypes.length > 0 || blocked > 0) { const failedTypes = verdict.requiredFailedTypes.join(', '); const whatFailed = describeDrainFailure(verdict); diff --git a/src/agent/wizard-ask-bridge.ts b/src/agent/wizard-ask-bridge.ts index bfc77cd86..5c57a1b79 100644 --- a/src/agent/wizard-ask-bridge.ts +++ b/src/agent/wizard-ask-bridge.ts @@ -163,7 +163,6 @@ export function createWizardAskBridge( controller.abort(); }; opts.signal?.addEventListener('abort', cancelForAbort, { once: true }); - if (opts.signal?.aborted) cancelForAbort(); }); try { diff --git a/src/lib/programs/__tests__/run-agent-legacy.test.ts b/src/lib/programs/__tests__/run-agent-legacy.test.ts index 323771f22..73b40bf90 100644 --- a/src/lib/programs/__tests__/run-agent-legacy.test.ts +++ b/src/lib/programs/__tests__/run-agent-legacy.test.ts @@ -44,6 +44,7 @@ vi.mock('@utils/analytics', () => ({ build: 'test', runId: 'run-1', wizardCapture: vi.fn(), + captureException: vi.fn(), setTag: vi.fn(), getAllFlagsForWizard: vi.fn().mockResolvedValue({}), getWizardFlagPayloads: vi.fn().mockReturnValue({}), @@ -201,9 +202,12 @@ it('clamps a composed program to linear and keeps host analytics alive', async ( expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith('success'); }); -it.each([RunOutcome.Aborted, RunOutcome.Failed] as const)( - 'passes a %s result to the existing abort handler', - async (outcome) => { +it.each([ + [RunOutcome.Aborted, 'cancelled'], + [RunOutcome.Failed, 'error'], +] as const)( + 'passes a %s result to the existing abort handler as %s', + async (outcome, status) => { const failure = { code: ErrorCodes.AgentApiError, message: 'Failed', @@ -211,11 +215,62 @@ it.each([RunOutcome.Aborted, RunOutcome.Failed] as const)( }; vi.mocked(runAgent).mockResolvedValue({ outcome, failure, snapshot }); await runProgramAgent(program(), session()); - expect(wizardAbort).toHaveBeenCalledExactlyOnceWith(failure); + expect(wizardAbort).toHaveBeenCalledExactlyOnceWith({ ...failure, status }); expect(analytics.shutdown).not.toHaveBeenCalled(); }, ); +it.each([ + [ + RunOutcome.Failed, + 'error', + { code: ErrorCodes.AgentMcpMissing, message: 'Could not access MCP' }, + ], + [ + RunOutcome.Aborted, + 'cancelled', + { code: ErrorCodes.AgentAbort, message: 'Agent run cancelled' }, + ], +] as const)( + 'labels a %s run %s from its outcome when no Error came back', + async (outcome, status, failure) => { + const actual = await vi.importActual( + '@utils/wizard-abort', + ); + vi.mocked(wizardAbort).mockImplementationOnce(actual.wizardAbort); + const exit = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + const stderr = vi + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); + vi.mocked(runAgent).mockResolvedValue({ + outcome, + failure: { ...failure }, + snapshot, + }); + try { + await runProgramAgent(program(), session()); + } finally { + exit.mockRestore(); + stderr.mockRestore(); + } + expect(analytics.shutdown).toHaveBeenCalledExactlyOnceWith(status); + if (status === 'error') { + // Error tracking still sees the failure, as its code and message. + expect(analytics.captureException).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + message: failure.message, + code: failure.code, + }), + { error_code: failure.code }, + ); + } else { + expect(analytics.captureException).not.toHaveBeenCalled(); + } + }, +); + it('shows the auth guidance from a decided 401 before the error outro', async () => { const detail = { hasSettingsConflict: false, logFilePath: '/tmp/wizard.log' }; const show = vi.spyOn(getUI(), 'showAuthError'); diff --git a/src/lib/programs/run-agent-legacy.ts b/src/lib/programs/run-agent-legacy.ts index fc2451319..4b74621cc 100644 --- a/src/lib/programs/run-agent-legacy.ts +++ b/src/lib/programs/run-agent-legacy.ts @@ -7,8 +7,8 @@ * authenticates, resolves the program's binding, builds the agent's inputs * from the session, maps every progress event back onto `getUI()` one call * per event, answers the agent's questions through `getUI()`, and applies the - * result — `wizardAbort` for a decided failure, the terminal analytics event - * for a finished top-level run. + * result — `wizardAbort` with the outcome's terminal status for a decided + * failure, the terminal analytics event for a finished top-level run. * * This is the only file that knows about `getUI()`, the session and * `wizardAbort` on the agent's behalf. Programs replace it in Release B. @@ -286,7 +286,11 @@ async function runProgram( if (result.failure.authErrorDetail) { ui.showAuthError(result.failure.authErrorDetail); } - await wizardAbort(result.failure); + // The terminal status follows how the run ended, not whether an Error came back. + await wizardAbort({ + ...result.failure, + status: result.outcome === RunOutcome.Aborted ? 'cancelled' : 'error', + }); } else if (!composed) { // A composed sub-run leaves the terminal event to its host program's run. await analytics.shutdown('success'); diff --git a/src/shared/utils/wizard-abort.ts b/src/shared/utils/wizard-abort.ts index 5b55b6864..0a2b8ae30 100644 --- a/src/shared/utils/wizard-abort.ts +++ b/src/shared/utils/wizard-abort.ts @@ -29,6 +29,8 @@ interface WizardAbortOptions { exitCode?: number; code?: ErrorCode; detail?: Record; + /** Terminal analytics status. Defaults from whether `error` is set. */ + status?: 'error' | 'cancelled'; } const cleanupFns: Array<() => void> = []; @@ -87,16 +89,23 @@ export async function wizardAbort( // 1. Run registered cleanup functions runCleanups(); - // 2. Capture error in analytics (if provided) - if (error) { - analytics.captureException(error, { - ...((error instanceof WizardError && error.context) || {}), + // 2. Capture error in analytics. An 'error' ending with no Error object + // is captured as its code and message. + const status = options?.status ?? (error ? 'error' : 'cancelled'); + const captured = + error ?? + (status === 'error' + ? new WizardError(message, undefined, code) + : undefined); + if (captured) { + analytics.captureException(captured, { + ...((captured instanceof WizardError && captured.context) || {}), ...(code ? { error_code: code } : {}), }); } // 3. Shutdown analytics - await analytics.shutdown(error ? 'error' : 'cancelled'); + await analytics.shutdown(status); // 4. Render the error outro. Synthesize OutroData from `message` // when the caller didn't provide structured data. From 0ceca0b3b742ccb46ee67557e79fe0844d66c202 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 12:15:46 -0400 Subject: [PATCH 42/61] docs: correct stage comments, the shared README and the detection runbook - Two agent entry comments said the bindings leave in B1. B1 deferred PROGRAM_BINDINGS to B2, so they say B2. - The shared README said claude-settings.ts and errors/agent-map.ts import agent leaf modules. Neither does any more. The one agent import left in shared is the type-only InstallSkillResult in errors/skill-map.ts. - The agentic detection runbook cited the removed AGENTIC_DETECTION_TIMEOUT_MS. Since #1305 the scan gets two attempts, 60s then 90s. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- docs/runbooks/agentic-monorepo-detection.md | 4 +++- src/agent/index.ts | 3 ++- src/agent/types.ts | 2 +- src/shared/README.md | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/runbooks/agentic-monorepo-detection.md b/docs/runbooks/agentic-monorepo-detection.md index f342de891..6bb3f3212 100644 --- a/docs/runbooks/agentic-monorepo-detection.md +++ b/docs/runbooks/agentic-monorepo-detection.md @@ -17,7 +17,9 @@ it. - We take the recommended supported project, else the first supported PostHog-free one, and re-point the install dir there. - On a single-repo project it recommends `.`, so nothing moves. -- If the scan errors, times out (60s, `AGENTIC_DETECTION_TIMEOUT_MS`), or finds +- The scan gets two attempts, 60s then 90s + (`AGENTIC_DETECTION_FIRST_ATTEMPT_TIMEOUT_MS`, + `AGENTIC_DETECTION_RETRY_TIMEOUT_MS`). If both error or time out, or it finds nothing, `session.installDir` is left untouched and the run falls back to the old root detection, exactly like flag-off. diff --git a/src/agent/index.ts b/src/agent/index.ts index 02937add6..d0ec4a5dc 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -19,7 +19,8 @@ export { AgentSignals } from './agent-interface'; export { WIZARD_TOOL_NAMES } from './tools'; /** - * Leaves in B1. Bindings and program data move to programs: resolveBinding + * Leaves in B2 (B1 deferred the bindings). Bindings and program data move to + * programs: resolveBinding * is keyed by PROGRAM_BINDINGS and the agent keeps only "run from an * already-resolved binding"; shouldDisableAsk is a flags policy programs * decide and pass in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own diff --git a/src/agent/types.ts b/src/agent/types.ts index d9a4b42a8..8fc7685d2 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -28,7 +28,7 @@ export type { TokenUsageDelta, } from './progress'; -/** Leaves in B1 with the bindings table. */ +/** Leaves in B2 with the bindings table; B1 deferred PROGRAM_BINDINGS. */ export type { ProgramBinding, SwitchboardCtx } from './runner'; /** Leaves in B2 with downloadSkill. */ diff --git a/src/shared/README.md b/src/shared/README.md index ae2a39c25..58c3d5850 100644 --- a/src/shared/README.md +++ b/src/shared/README.md @@ -40,4 +40,4 @@ Shared exists so the agent, programs, TUI, headless and CLI code can use one imp ## Architecture -Shared imports `src/env.ts` and itself. The architecture test classifies `src/shared` as its own surface and lists the remaining upward edges in `src/__tests__/architecture/known-violations.json`; each has an owner in the stack plan. `utils/setup-utils.ts`, `utils/oauth.ts` and `utils/wizard-abort.ts` are TUI and CLI flow code that leave in Release C; `utils/analytics.ts` reads the session until Release B; `claude-settings.ts` and `errors/agent-map.ts` import two agent leaf modules until Release B, because the agent entry would form a module cycle through analytics. +Shared imports `src/env.ts` and itself. The architecture test classifies `src/shared` as its own surface and lists the remaining upward edges in `src/__tests__/architecture/known-violations.json`; each has an owner in the stack plan. `utils/setup-utils.ts`, `utils/oauth.ts` and `utils/wizard-abort.ts` are TUI and CLI flow code that leave in Release C; `utils/analytics.ts` reads the session until Release B. The only agent import left is type-only: `errors/skill-map.ts` takes `InstallSkillResult` from `@agent/types`. `claude-settings.ts` now uses `agent-env-isolation.ts` in shared, and the agent error map lives in `src/agent/error-map.ts`. From dcf418af73812da76c6deddbd7813969421e0bfa Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 13:11:25 -0400 Subject: [PATCH 43/61] fix(lint): fence directory imports out of the agent too The agent fence matched files inside ui, detection, runners and steps but not the directories themselves, so `../../ui` or `@lib/detection` passed lint. Relative steps imports were not covered at all. A probe with eight such imports gave 1 error before and 8 after. The fence comment now says that only direct static imports are checked; shared helpers still reach the UI transitively. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .eslintrc.cjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e3fa6cb1d..a633af786 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -33,10 +33,10 @@ module.exports = { { // The agent surface. It takes resolved data in, reports through // progress events and asks through an injected answerer, so nothing - // here may reach a UI, the session, detection, the CLI or a program at - // runtime. Program types stay importable until B1 moves - // PROGRAM_BINDINGS to programs. Today's paths; A2b collapses them to - // src/agent/**. + // here may import a UI, the session, detection, the CLI or a program. + // Only direct static imports are checked. Program types stay importable + // until B1 moves PROGRAM_BINDINGS to programs. Today's paths; A2b + // collapses them to src/agent/**. files: [ 'src/lib/agent/**/*.ts', 'src/lib/middleware/**/*.ts', @@ -65,18 +65,25 @@ module.exports = { group: [ '@ui', '@ui/**', + '**/ui', '**/ui/**', '@lib/wizard-session', '**/wizard-session', + '@lib/detection', '@lib/detection/**', + '**/detection', '**/detection/**', '@lib/registry', '**/lib/registry', + '@lib/runners', '@lib/runners/**', + '**/runners', '**/runners/**', '**/commands/**', '@steps', '@steps/**', + '**/steps', + '**/steps/**', '@frameworks/**', '**/frameworks/**', '@utils/setup-utils', From 3c7dfb5afe9ea68587dbd6e1b5a303b6549d1fcc Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 13:11:36 -0400 Subject: [PATCH 44/61] test(agent): cover the handoff, benchmark and scan-summary wiring Each of these reaches the host through a callback the runner or a tool facade passes on, and the existing tests supplied their own callback. The new tests go through the real wiring: both registered publish_handoff tools (Pi and MCP), a linear run with --benchmark, and a scan summary on completion, agent abort and crash. Dropping any of those callbacks now fails a test; each was checked by removing it. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__tests__/run-agent-standalone.test.ts | 87 +++++++++++++++++ .../__tests__/handoff-tools.test.ts | 96 +++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 src/lib/wizard-tools/__tests__/handoff-tools.test.ts diff --git a/src/lib/agent/__tests__/run-agent-standalone.test.ts b/src/lib/agent/__tests__/run-agent-standalone.test.ts index 2f03ff3fe..13756a9ec 100644 --- a/src/lib/agent/__tests__/run-agent-standalone.test.ts +++ b/src/lib/agent/__tests__/run-agent-standalone.test.ts @@ -131,6 +131,15 @@ vi.mock('@lib/agent/runner/switchboard/harness', () => { }, }); await askIfRequested(inputs); + // A real harness feeds the benchmark middleware every SDK message. + inputs.middleware?.onMessage({ + type: 'assistant', + message: { role: 'assistant', content: [{ type: 'text', text: 'hi' }] }, + }); + inputs.middleware?.finalize( + { type: 'result', modelUsage: {}, num_turns: 1 }, + 10, + ); if (harnessState.throws) throw harnessState.throws; spinner.stop('Done'); return harnessState.result as never; @@ -685,4 +694,82 @@ describe('runAgent standalone', () => { // What was reported before the crash survives in the snapshot. expect(result.snapshot.statusMessages).toContain('Installing the SDK'); }); + + it('sends benchmark output to onProgress when benchmarking', async () => { + const benchmarkPath = path.join(tmp, 'benchmark.json'); + const configPath = path.join(tmp, '.benchmark-config.json'); + fs.writeFileSync( + configPath, + JSON.stringify({ output: { benchmarkPath, logEnabled: false } }), + ); + vi.stubEnv('POSTHOG_WIZARD_BENCHMARK_CONFIG', configPath); + vi.stubEnv('POSTHOG_WIZARD_BENCHMARK_FILE', benchmarkPath); + vi.stubEnv('POSTHOG_WIZARD_LOG_DIR', tmp); + const runInput = input(); + runInput.flags.benchmark = true; + const events: AgentProgress[] = []; + try { + const result = await runAgent(config(), runInput, { + onProgress: (e) => events.push(e), + }); + + expect(result.outcome).toBe('success'); + const logs = events.flatMap((e) => (e.kind === 'log' ? [e.message] : [])); + expect(logs).toContainEqual( + expect.stringContaining( + `Benchmark data will be written to: ${benchmarkPath}`, + ), + ); + expect(logs).toContainEqual( + expect.stringContaining(`Results written to ${benchmarkPath}`), + ); + expect(fs.existsSync(benchmarkPath)).toBe(true); + } finally { + vi.unstubAllEnvs(); + } + }); + + it.each<[string, () => void, string]>([ + ['completes', () => undefined, 'success'], + [ + 'aborts', + () => { + harnessState.result = { + error: AgentErrorType.ABORT, + message: 'No Stripe found', + }; + }, + 'aborted', + ], + [ + 'crashes', + () => { + harnessState.throws = new Error('SDK exploded'); + }, + 'crashed', + ], + ])( + 'sends the scan summary to onProgress when the run %s', + async (_ending, arrange, outcome) => { + const summary = + 'YARA scan report: /tmp/yara.json\n— YARA Scanner Summary —'; + vi.mocked(flushScanReport).mockReturnValueOnce(summary); + arrange(); + const runInput = input(); + runInput.flags.yaraReport = true; + const events: AgentProgress[] = []; + + const result = await runAgent(config(), runInput, { + onProgress: (e) => events.push(e), + }); + + expect(result.outcome).toBe(outcome); + expect(flushScanReport).toHaveBeenCalledWith({ yaraReport: true }); + expect(events).toContainEqual({ + kind: 'log', + level: 'info', + message: summary, + }); + }, + ); }); diff --git a/src/lib/wizard-tools/__tests__/handoff-tools.test.ts b/src/lib/wizard-tools/__tests__/handoff-tools.test.ts new file mode 100644 index 000000000..6f3b6f062 --- /dev/null +++ b/src/lib/wizard-tools/__tests__/handoff-tools.test.ts @@ -0,0 +1,96 @@ +// Both registered publish_handoff tools, Pi and MCP, must hand the report to the host. +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { LLMProvider } from '@posthog/warlock'; +import type { AgentProgress } from '@lib/agent/progress'; +import { createWizardPiTools } from '@lib/agent/runner/harness/pi/tools'; +import { createWizardToolsServer } from '../mcp'; +import { PUBLISH_HANDOFF_TOOL_NAME } from '../handoff'; + +vi.mock('@ui', () => ({ + getUI: () => { + throw new Error('agent code reached the UI'); + }, +})); +// The MCP server's tool list, without the SDK's transport around it. +vi.mock('@anthropic-ai/claude-agent-sdk', () => ({ + tool: ( + name: string, + description: string, + inputSchema: unknown, + handler: (args: unknown) => unknown, + ) => ({ name, description, inputSchema, handler }), + createSdkMcpServer: (options: unknown) => options, +})); +vi.mock('../tools', async (original) => ({ + ...(await original()), + fetchSkillMenu: vi.fn().mockResolvedValue(null), +})); + +const REPORT = '# Setup report\n\nAll done.'; + +describe('registered publish_handoff tools', () => { + let workingDirectory: string; + let events: AgentProgress[]; + let ambientOutputPath: string | undefined; + + beforeEach(() => { + workingDirectory = mkdtempSync(join(tmpdir(), 'wizard-handoff-tools-')); + events = []; + ambientOutputPath = process.env.POSTHOG_HANDOFF_OUTPUT_PATH; + delete process.env.POSTHOG_HANDOFF_OUTPUT_PATH; + }); + + afterEach(() => { + rmSync(workingDirectory, { recursive: true, force: true }); + if (ambientOutputPath === undefined) { + delete process.env.POSTHOG_HANDOFF_OUTPUT_PATH; + } else { + process.env.POSTHOG_HANDOFF_OUTPUT_PATH = ambientOutputPath; + } + }); + + const handoffs = () => + events.flatMap((event) => (event.kind === 'handoff' ? [event.text] : [])); + + it('the Pi tool reports the handoff to the host', async () => { + const tools = createWizardPiTools({ + workingDirectory, + skillsBaseUrl: 'http://localhost:0', + emit: (event) => events.push(event), + }); + const tool = tools.find((t) => t.name === PUBLISH_HANDOFF_TOOL_NAME); + if (!tool) throw new Error('publish_handoff not registered'); + + const result = (await ( + tool.execute as (id: string, args: unknown) => Promise + )('call-1', { content: REPORT })) as { content: [{ text: string }] }; + + expect(result.content[0].text).toContain('Handoff published'); + expect(handoffs()).toEqual([REPORT]); + }); + + it('the MCP tool reports the handoff to the host', async () => { + const server = (await createWizardToolsServer({ + workingDirectory, + detectPackageManager: vi.fn(), + skillsBaseUrl: 'http://localhost:0', + triageProvider: {} as LLMProvider, + emit: (event) => events.push(event), + })) as unknown as { + tools: { name: string; handler: (args: unknown) => unknown }[]; + }; + const tool = server.tools.find((t) => t.name === PUBLISH_HANDOFF_TOOL_NAME); + if (!tool) throw new Error('publish_handoff not registered'); + + const result = (await tool.handler({ content: REPORT })) as { + content: [{ text: string }]; + isError?: boolean; + }; + + expect(result.isError).toBeUndefined(); + expect(result.content[0].text).toContain('Handoff published'); + expect(handoffs()).toEqual([REPORT]); + }); +}); From 38f57e8024d67d95fae121a5c8984a60e09c0d70 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 23 Sep 2026 13:18:29 -0400 Subject: [PATCH 45/61] fix(agent): report the scan summary when a run is cancelled before it starts runAgent's pre-aborted return flushed the scan report but dropped the summary line it returned, so the report file could be written with no line in the terminal. Every termination path now flushes through one helper that emits the line as a log event. The scan-summary test table gains the two host-cancel cases, mid-run and before start; the second failed before this change. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__tests__/run-agent-standalone.test.ts | 40 +++++++++++++++++-- src/agent/runner/index.ts | 23 +++++------ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index 160885c51..a6ac6444d 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -208,7 +208,7 @@ vi.mock('@shared/skill-menu', async (original) => ({ })); import { runAgent, RunOutcome } from '@agent/runner'; -import type { RunConfig, RunInput } from '@agent/runner'; +import type { RunAgentOptions, RunConfig, RunInput } from '@agent/runner'; import { analytics } from '@utils/analytics'; import { initLogFile } from '@utils/debug'; import { flushScanReport } from '@agent/yara-hooks'; @@ -1029,8 +1029,10 @@ describe('runAgent standalone', () => { } }); - it.each<[string, () => void, string]>([ - ['completes', () => undefined, 'success'], + it.each< + [string, (host: AbortController) => Partial, string] + >([ + ['completes', () => ({}), 'success'], [ 'stops itself', () => { @@ -1039,6 +1041,7 @@ describe('runAgent standalone', () => { classification: AgentErrorType.ABORT, message: 'No Stripe found', }; + return {}; }, 'failed', ], @@ -1046,21 +1049,50 @@ describe('runAgent standalone', () => { 'crashes', () => { harnessState.throws = new Error('SDK exploded'); + return {}; }, 'crashed', ], + [ + 'is cancelled mid-run', + (host) => { + harnessState.askQuestions = [ + { id: 'q1', prompt: 'Continue?', kind: 'text' }, + ]; + return { + interaction: { + ask: () => { + host.abort(); + return new Promise(() => undefined); + }, + }, + }; + }, + 'aborted', + ], + [ + 'is cancelled before it starts', + (host) => { + host.abort(); + return {}; + }, + 'aborted', + ], ])( 'sends the scan summary to onProgress when the run %s', async (_ending, arrange, outcome) => { const summary = 'YARA scan report: /tmp/yara.json\n— YARA Scanner Summary —'; vi.mocked(flushScanReport).mockReturnValueOnce(summary); - arrange(); + const host = new AbortController(); + const options = arrange(host); const runInput = input(); runInput.flags.yaraReport = true; const events: AgentProgress[] = []; const result = await runAgent(config(), runInput, { + ...options, + signal: host.signal, onProgress: (e) => events.push(e), }); diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index cd98f7725..8f2e92330 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -95,6 +95,15 @@ export async function runAgent( }, }; }; + const flushReport = (): void => { + try { + const report = flushScanReport({ yaraReport: input.flags.yaraReport }); + if (report) + collector?.emit({ kind: 'log', level: 'info', message: report }); + } catch { + // Scan reporting is best effort after the run outcome is decided. + } + }; let result: RunResult; try { collector = createProgressCollector(options.onProgress); @@ -102,11 +111,7 @@ export async function runAgent( const log = (message: string) => emit({ kind: 'log', level: 'info', message }); if (options.signal?.aborted) { - try { - flushScanReport({ yaraReport: input.flags.yaraReport }); - } catch { - // Scan reporting is best effort. - } + flushReport(); return { outcome: RunOutcome.Aborted, skillId: input.skillId, @@ -209,13 +214,7 @@ export async function runAgent( }; } } - try { - const report = flushScanReport({ yaraReport: input.flags.yaraReport }); - if (report) - collector?.emit({ kind: 'log', level: 'info', message: report }); - } catch { - // Scan reporting is best effort after the run outcome is decided. - } + flushReport(); return result; } From edbf1f9ebdba38c2177cbd23cd1a5b1707c590f8 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:33:02 -0400 Subject: [PATCH 46/61] docs(shared): analytics reads the session until C2a Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/shared/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/README.md b/src/shared/README.md index 58c3d5850..4bbd24aab 100644 --- a/src/shared/README.md +++ b/src/shared/README.md @@ -40,4 +40,4 @@ Shared exists so the agent, programs, TUI, headless and CLI code can use one imp ## Architecture -Shared imports `src/env.ts` and itself. The architecture test classifies `src/shared` as its own surface and lists the remaining upward edges in `src/__tests__/architecture/known-violations.json`; each has an owner in the stack plan. `utils/setup-utils.ts`, `utils/oauth.ts` and `utils/wizard-abort.ts` are TUI and CLI flow code that leave in Release C; `utils/analytics.ts` reads the session until Release B. The only agent import left is type-only: `errors/skill-map.ts` takes `InstallSkillResult` from `@agent/types`. `claude-settings.ts` now uses `agent-env-isolation.ts` in shared, and the agent error map lives in `src/agent/error-map.ts`. +Shared imports `src/env.ts` and itself. The architecture test classifies `src/shared` as its own surface and lists the remaining upward edges in `src/__tests__/architecture/known-violations.json`; each has an owner in the stack plan. `utils/setup-utils.ts`, `utils/oauth.ts` and `utils/wizard-abort.ts` are TUI and CLI flow code that leave in Release C; `utils/analytics.ts` reads the session until C2a. The only agent import left is type-only: `errors/skill-map.ts` takes `InstallSkillResult` from `@agent/types`. `claude-settings.ts` now uses `agent-env-isolation.ts` in shared, and the agent error map lives in `src/agent/error-map.ts`. From 7f8f045b3c9c68637e29814ad400f1a4a22b7144 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:48:52 -0400 Subject: [PATCH 47/61] refactor(shared): move cleanup, scan consent, run tags and ask policy into shared Four stateless leaves move into src/shared with no behavior change: the cleanup registry out of wizard-abort, the scan-consent helpers and DiscoveredFeature out of the session, buildRunTags out of the agent interface, and the ask policy and longer ask timeout out of the runner bootstrap and the ask bridge. Each old module re-exports what it gave up, so shouldDisableAsk and every other existing import keeps working. The run-tags test moves with its function. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/agent-interface.ts | 25 +-------------- src/agent/runner/shared/bootstrap.ts | 24 ++------------ src/agent/wizard-ask-bridge.ts | 8 +---- src/lib/wizard-session.ts | 30 +++++------------- .../__tests__/run-tags.test.ts | 2 +- src/shared/ask-policy.ts | 31 +++++++++++++++++++ src/shared/run-tags.ts | 26 ++++++++++++++++ src/shared/scan-consent.ts | 26 ++++++++++++++++ src/shared/utils/cleanup-registry.ts | 22 +++++++++++++ src/shared/utils/wizard-abort.ts | 25 ++------------- 10 files changed, 120 insertions(+), 99 deletions(-) rename src/{agent => shared}/__tests__/run-tags.test.ts (97%) create mode 100644 src/shared/ask-policy.ts create mode 100644 src/shared/run-tags.ts create mode 100644 src/shared/scan-consent.ts create mode 100644 src/shared/utils/cleanup-registry.ts diff --git a/src/agent/agent-interface.ts b/src/agent/agent-interface.ts index 005315236..f19cd85f2 100644 --- a/src/agent/agent-interface.ts +++ b/src/agent/agent-interface.ts @@ -365,30 +365,7 @@ type AgentRunConfig = { const NO_PROGRESS: ProgressEmitter = () => undefined; -/** - * Global identifiers attached to every LLM gateway trace for a run. They ride on - * each `$ai_generation` the gateway emits (in the `X-PostHog-Properties` blob - * `buildAgentEnv` builds), so traces are filterable by program, framework, run, - * and build type for cost attribution and dashboards. `skill_id` is omitted when - * the run has none. - */ -export function buildRunTags(args: { - programId: string; - integration: string; - runId: string; - build: string; - skillId?: string; -}): Record { - return { - program_id: args.programId, - integration: args.integration, - run_id: args.runId, - build: args.build, - // Triage and detection spread these tags and override this one. - call_type: CallType.agent, - ...(args.skillId ? { skill_id: args.skillId } : {}), - }; -} +export { buildRunTags } from '@shared/run-tags'; /** * Whether Warlock/YARA scanning is disabled for this run. Off by default: diff --git a/src/agent/runner/shared/bootstrap.ts b/src/agent/runner/shared/bootstrap.ts index 1ed76bae0..4bb164dda 100644 --- a/src/agent/runner/shared/bootstrap.ts +++ b/src/agent/runner/shared/bootstrap.ts @@ -15,31 +15,11 @@ import { CallType, IS_DEV } from '@shared/constants'; import { VERSION } from '@shared/version'; import { mcpUrlFor } from '@shared/host-resolution'; import type { WizardRunOptions } from '@utils/types'; -import type { BootstrapResult, RunConfig, RunFlags, RunInput } from './types'; +import type { BootstrapResult, RunConfig, RunInput } 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. - * - * `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( - flags: Pick, -): boolean { - return (flags.ci || flags.signup) && !flags.e2eAsk; -} +export { isAskDisabled as shouldDisableAsk } from '@shared/ask-policy'; /** The option bag the agent interface and the middleware read. */ export function runOptions(input: RunInput): WizardRunOptions { diff --git a/src/agent/wizard-ask-bridge.ts b/src/agent/wizard-ask-bridge.ts index 5c57a1b79..e03b5c91b 100644 --- a/src/agent/wizard-ask-bridge.ts +++ b/src/agent/wizard-ask-bridge.ts @@ -92,13 +92,7 @@ export const CANCELLED_SENTINEL = '__cancelled__'; /** Default per-question timeout (5 minutes). */ export const DEFAULT_ASK_TIMEOUT_MS = 5 * 60 * 1000; -/** - * The longer per-question timeout, for asks that send the user on an errand — - * open a database console, mint a restricted API key. The default above is - * sized for a question answerable from memory and expires long before an - * errand is done. - */ -export const LONGER_ASK_TIMEOUT_MS = 20 * 60 * 1000; +export { LONGER_ASK_TIMEOUT_MS } from '@shared/ask-policy'; function buildCancelledAnswers(questions: AskQuestion[]): AskAnswers { const out: AskAnswers = {}; diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 4739c3aa6..2d9a0dca2 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -11,6 +11,7 @@ */ import { POSTHOG_LOCAL_URL, resolveLocalDev } from '@shared/local-dev'; +import { DiscoveredFeature } from '@shared/scan-consent'; import { AdditionalFeature, ADDITIONAL_FEATURE_LABELS, @@ -67,11 +68,7 @@ export enum RunPhase { Error = 'error', } -/** Features discovered by the feature-discovery subagent */ -export enum DiscoveredFeature { - Stripe = 'stripe', - LLM = 'llm', -} +export { DiscoveredFeature }; /** Consent to report what local detection found (see `scanConsent` below). */ export enum ScanConsent { @@ -458,21 +455,8 @@ export function buildSession(args: { }; } -/** One place to ask, so a new consent state does not need three edits. */ -export function mayReportScanResults(session: WizardSession): boolean { - return session.scanConsent === ScanConsent.Granted; -} - -/** Lives here so analytics infrastructure never learns what consent means. */ -export function reportableDiscoveredFeatures( - session: WizardSession, -): DiscoveredFeature[] | undefined { - return mayReportScanResults(session) ? session.discoveredFeatures : undefined; -} - -/** Also a scan result, so it travels under the same consent as the rest. */ -export function reportablePosthogSdkDetected( - session: WizardSession, -): boolean | undefined { - return mayReportScanResults(session) ? session.posthogSdkDetected : undefined; -} +export { + mayReportScanResults, + reportableDiscoveredFeatures, + reportablePosthogSdkDetected, +} from '@shared/scan-consent'; diff --git a/src/agent/__tests__/run-tags.test.ts b/src/shared/__tests__/run-tags.test.ts similarity index 97% rename from src/agent/__tests__/run-tags.test.ts rename to src/shared/__tests__/run-tags.test.ts index 8211b96e8..9a01156bb 100644 --- a/src/agent/__tests__/run-tags.test.ts +++ b/src/shared/__tests__/run-tags.test.ts @@ -1,4 +1,4 @@ -import { buildRunTags } from '@agent/agent-interface'; +import { buildRunTags } from '@shared/run-tags'; import { CallType } from '@shared/constants'; describe('buildRunTags', () => { diff --git a/src/shared/ask-policy.ts b/src/shared/ask-policy.ts new file mode 100644 index 000000000..5266e499c --- /dev/null +++ b/src/shared/ask-policy.ts @@ -0,0 +1,31 @@ +/** When a run may put a `wizard_ask` question to a person, and how long it waits. */ + +/** + * Whether the `wizard_ask` overlay stays unwired for this run. Non-interactive + * modes (CI, signup) have no human to answer. Per-program disabling adds + * WIZARD_ASK_TOOL_NAME to the program's `disallowedTools` instead, so the SDK + * rejects calls outright. + * + * `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 keep the overlay unwired. + */ +export function isAskDisabled(flags: { + ci: boolean; + signup: boolean; + e2eAsk: boolean; +}): boolean { + return (flags.ci || flags.signup) && !flags.e2eAsk; +} + +/** + * The longer per-question timeout, for asks that send the user on an errand — + * open a database console, mint a restricted API key. The default is sized for + * a question answerable from memory and expires long before an errand is done. + */ +export const LONGER_ASK_TIMEOUT_MS = 20 * 60 * 1000; diff --git a/src/shared/run-tags.ts b/src/shared/run-tags.ts new file mode 100644 index 000000000..144d4b638 --- /dev/null +++ b/src/shared/run-tags.ts @@ -0,0 +1,26 @@ +import { CallType } from '@shared/constants'; + +/** + * Global identifiers attached to every LLM gateway trace for a run. They ride on + * each `$ai_generation` the gateway emits (in the `X-PostHog-Properties` blob + * `buildAgentEnv` builds), so traces are filterable by program, framework, run, + * and build type for cost attribution and dashboards. `skill_id` is omitted when + * the run has none. + */ +export function buildRunTags(args: { + programId: string; + integration: string; + runId: string; + build: string; + skillId?: string; +}): Record { + return { + program_id: args.programId, + integration: args.integration, + run_id: args.runId, + build: args.build, + // Triage and detection spread these tags and override this one. + call_type: CallType.agent, + ...(args.skillId ? { skill_id: args.skillId } : {}), + }; +} diff --git a/src/shared/scan-consent.ts b/src/shared/scan-consent.ts new file mode 100644 index 000000000..9188cac6c --- /dev/null +++ b/src/shared/scan-consent.ts @@ -0,0 +1,26 @@ +/** Features discovered by the feature-discovery subagent */ +export enum DiscoveredFeature { + Stripe = 'stripe', + LLM = 'llm', +} + +type ScanConsentState = { scanConsent: string }; + +/** One place to ask, so a new consent state does not need three edits. */ +export function mayReportScanResults(session: ScanConsentState): boolean { + return session.scanConsent === 'granted'; +} + +/** Lives here so analytics infrastructure never learns what consent means. */ +export function reportableDiscoveredFeatures( + session: ScanConsentState & { discoveredFeatures: DiscoveredFeature[] }, +): DiscoveredFeature[] | undefined { + return mayReportScanResults(session) ? session.discoveredFeatures : undefined; +} + +/** Also a scan result, so it travels under the same consent as the rest. */ +export function reportablePosthogSdkDetected( + session: ScanConsentState & { posthogSdkDetected: boolean }, +): boolean | undefined { + return mayReportScanResults(session) ? session.posthogSdkDetected : undefined; +} diff --git a/src/shared/utils/cleanup-registry.ts b/src/shared/utils/cleanup-registry.ts new file mode 100644 index 000000000..071955574 --- /dev/null +++ b/src/shared/utils/cleanup-registry.ts @@ -0,0 +1,22 @@ +/** Process-local cleanup callbacks shared by the CLI and agent. */ +const cleanupFns: Array<() => void> = []; + +export function registerCleanup(fn: () => void): void { + cleanupFns.push(fn); +} + +export function clearCleanup(): void { + cleanupFns.length = 0; +} + +/** Runs all registered cleanup functions and drains the array. */ +export function runCleanups(): void { + const fns = cleanupFns.splice(0); + for (const fn of fns) { + try { + fn(); + } catch { + /* cleanup should not prevent exit */ + } + } +} diff --git a/src/shared/utils/wizard-abort.ts b/src/shared/utils/wizard-abort.ts index 0a2b8ae30..ec5275ac8 100644 --- a/src/shared/utils/wizard-abort.ts +++ b/src/shared/utils/wizard-abort.ts @@ -17,6 +17,9 @@ import { emitWizardError, sanitizeErrorDetail, } from '@shared/errors'; +import { runCleanups } from './cleanup-registry'; + +export { registerCleanup, clearCleanup, runCleanups } from './cleanup-registry'; // Still importable from here; the class lives with the error codes. export { WizardError }; @@ -33,28 +36,6 @@ interface WizardAbortOptions { status?: 'error' | 'cancelled'; } -const cleanupFns: Array<() => void> = []; - -export function registerCleanup(fn: () => void): void { - cleanupFns.push(fn); -} - -export function clearCleanup(): void { - cleanupFns.length = 0; -} - -/** Runs all registered cleanup functions and drains the array. */ -export function runCleanups(): void { - const fns = cleanupFns.splice(0); - for (const fn of fns) { - try { - fn(); - } catch { - /* cleanup should not prevent exit */ - } - } -} - function resolveErrorCode( options: WizardAbortOptions, error: Error | WizardError | undefined, From 57f1a82f9448bfc37119832e29c15c46165c9d71 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:50:41 -0400 Subject: [PATCH 48/61] refactor(shared): move the OAuth token helpers into oauth-token OAuthTokenResponseSchema, getOAuthClientId and refreshAccessToken need no UI, so they move from utils/oauth.ts into utils/oauth-token.ts with no behavior change. oauth.ts imports what its login flow uses and re-exports the schema, its type and refreshAccessToken for existing callers. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/shared/utils/oauth-token.ts | 84 +++++++++++++++++++++++++++++++ src/shared/utils/oauth.ts | 87 +++++---------------------------- 2 files changed, 95 insertions(+), 76 deletions(-) create mode 100644 src/shared/utils/oauth-token.ts diff --git a/src/shared/utils/oauth-token.ts b/src/shared/utils/oauth-token.ts new file mode 100644 index 000000000..92443d5c5 --- /dev/null +++ b/src/shared/utils/oauth-token.ts @@ -0,0 +1,84 @@ +/** OAuth token-endpoint helpers that need no UI: the token response and the refresh grant. */ +import axios from 'axios'; +import { z } from 'zod'; +import { + POSTHOG_DEV_CLIENT_ID, + POSTHOG_PROXY_CLIENT_ID, + WIZARD_USER_AGENT, +} from '@shared/constants'; +import { logToFile } from './debug'; +import { getOAuthUrl, resolveBaseUrl } from './urls'; +import { oauthErrorFromTokenBody } from './oauth-errors'; + +export const OAuthTokenResponseSchema = z.object({ + access_token: z.string(), + expires_in: z.number(), + token_type: z.string(), + scope: z.string(), + refresh_token: z.string().optional(), + scoped_teams: z.array(z.number()).optional(), + scoped_organizations: z.array(z.string()).optional(), + // Sent by PostHog Cloud (and passed through the oauth.posthog.com proxy); absent on + // self-hosted. `.catch(undefined)` so an unrecognized value degrades to the probe + // fallback instead of failing the whole login. + posthog_region: z.enum(['us', 'eu']).optional().catch(undefined), + posthog_base_url: z.string().optional().catch(undefined), +}); + +export type OAuthTokenResponse = z.infer; + +/** + * OAuth client ID for the current target. A pinned base URL (`--base-url`, or + * IS_DEV's implicit localhost) means we're talking to a dev-seeded stack, which + * registers the dev client; prod uses the proxy client. + * + * TODO: this assumes any pinned base URL is a dev-seeded instance that + * registers POSTHOG_DEV_CLIENT_ID. If we ever point `--base-url` at a non-dev + * instance with its own OAuth app, make the client ID configurable (e.g. a + * `--oauth-client-id` flag) instead of always falling back to the dev client. + */ +export function getOAuthClientId(baseUrl?: string): string { + return resolveBaseUrl(baseUrl) + ? POSTHOG_DEV_CLIENT_ID + : POSTHOG_PROXY_CLIENT_ID; +} + +// Refresh-token grant (RFC 6749 §6); the server rotates, so callers must store the returned refresh_token. +export async function refreshAccessToken( + refreshToken: string, + baseUrl?: string, + clientId?: string, +): Promise { + const oauthUrl = getOAuthUrl(baseUrl); + logToFile(`[oauth] refreshing access token at ${oauthUrl}/oauth/token`); + try { + const response = await axios.post( + `${oauthUrl}/oauth/token`, + { + grant_type: 'refresh_token', + refresh_token: refreshToken, + // The grant only refreshes under its minting app — provisioning signups pass their regional client. + client_id: clientId ?? getOAuthClientId(baseUrl), + }, + { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': WIZARD_USER_AGENT, + }, + timeout: 30_000, + }, + ); + const token = OAuthTokenResponseSchema.parse(response.data); + logToFile('[oauth] access token refreshed'); + return token; + } catch (e) { + logToFile( + '[oauth] token refresh failed:', + e instanceof Error ? e.message : e, + ); + const refreshError = axios.isAxiosError(e) + ? oauthErrorFromTokenBody(e.response?.data) + : null; + throw refreshError ?? e; + } +} diff --git a/src/shared/utils/oauth.ts b/src/shared/utils/oauth.ts index dda07db81..5ac3f5934 100644 --- a/src/shared/utils/oauth.ts +++ b/src/shared/utils/oauth.ts @@ -3,13 +3,10 @@ import * as http from 'node:http'; import { execSync } from 'node:child_process'; import axios from 'axios'; import { logToFile } from './debug'; -import { z } from 'zod'; import { getUI } from '@ui'; import { OAUTH_PORTS, OAUTH_TIMEOUT_MS, - POSTHOG_DEV_CLIENT_ID, - POSTHOG_PROXY_CLIENT_ID, WIZARD_USER_AGENT, } from '@shared/constants'; import { getOAuthUrl, resolveBaseUrl } from './urls'; @@ -23,6 +20,17 @@ import { oauthErrorFromCallbackParams, oauthErrorFromTokenBody, } from './oauth-errors'; +import { + getOAuthClientId, + OAuthTokenResponseSchema, + type OAuthTokenResponse, +} from './oauth-token'; + +export { + OAuthTokenResponseSchema, + refreshAccessToken, + type OAuthTokenResponse, +} from './oauth-token'; const OAUTH_CALLBACK_STYLES = ` `; -export const OAuthTokenResponseSchema = z.object({ - access_token: z.string(), - expires_in: z.number(), - token_type: z.string(), - scope: z.string(), - refresh_token: z.string().optional(), - scoped_teams: z.array(z.number()).optional(), - scoped_organizations: z.array(z.string()).optional(), - // Sent by PostHog Cloud (and passed through the oauth.posthog.com proxy); absent on - // self-hosted. `.catch(undefined)` so an unrecognized value degrades to the probe - // fallback instead of failing the whole login. - posthog_region: z.enum(['us', 'eu']).optional().catch(undefined), - posthog_base_url: z.string().optional().catch(undefined), -}); - -export type OAuthTokenResponse = z.infer; - export const WIZARD_COMPLETION_SCOPE = 'event_definition:write'; export function parseOAuthScopes(scope: string): string[] { @@ -117,22 +108,6 @@ interface OAuthConfig { baseUrl?: string; } -/** - * OAuth client ID for the current target. A pinned base URL (`--base-url`, or - * IS_DEV's implicit localhost) means we're talking to a dev-seeded stack, which - * registers the dev client; prod uses the proxy client. - * - * TODO: this assumes any pinned base URL is a dev-seeded instance that - * registers POSTHOG_DEV_CLIENT_ID. If we ever point `--base-url` at a non-dev - * instance with its own OAuth app, make the client ID configurable (e.g. a - * `--oauth-client-id` flag) instead of always falling back to the dev client. - */ -function getOAuthClientId(baseUrl?: string): string { - return resolveBaseUrl(baseUrl) - ? POSTHOG_DEV_CLIENT_ID - : POSTHOG_PROXY_CLIENT_ID; -} - function getLocalOAuthOrigin(port: number): string { return `http://localhost:${port}`; } @@ -467,46 +442,6 @@ async function exchangeCodeForToken( return token; } -// Refresh-token grant (RFC 6749 §6); the server rotates, so callers must store the returned refresh_token. -export async function refreshAccessToken( - refreshToken: string, - baseUrl?: string, - clientId?: string, -): Promise { - const oauthUrl = getOAuthUrl(baseUrl); - logToFile(`[oauth] refreshing access token at ${oauthUrl}/oauth/token`); - try { - const response = await axios.post( - `${oauthUrl}/oauth/token`, - { - grant_type: 'refresh_token', - refresh_token: refreshToken, - // The grant only refreshes under its minting app — provisioning signups pass their regional client. - client_id: clientId ?? getOAuthClientId(baseUrl), - }, - { - headers: { - 'Content-Type': 'application/json', - 'User-Agent': WIZARD_USER_AGENT, - }, - timeout: 30_000, - }, - ); - const token = OAuthTokenResponseSchema.parse(response.data); - logToFile('[oauth] access token refreshed'); - return token; - } catch (e) { - logToFile( - '[oauth] token refresh failed:', - e instanceof Error ? e.message : e, - ); - const refreshError = axios.isAxiosError(e) - ? oauthErrorFromTokenBody(e.response?.data) - : null; - throw refreshError ?? e; - } -} - /** * Warn — at login, while the user is still watching — when the grant came back * narrower than the request, and record the gap so narrowed runs are countable. From acecf6b74a3bd7ec0f4b9b165be895182cc43a14 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:52:26 -0400 Subject: [PATCH 49/61] refactor(shared): move the gateway bearer type and checks into gateway-auth GatewayAuth, isPastRefresh, isTrustedGatewayUrl and buildWizardPropertiesBlob hold no mint state, so they move verbatim from agent/gateway-session.ts into shared/gateway-auth.ts. The mint, its cache and the CI credential loading stay where they are. gateway-session.ts imports what the mint uses and re-exports all four, so no caller changes. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/gateway-session.ts | 104 +++-------------------------------- src/shared/gateway-auth.ts | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 97 deletions(-) create mode 100644 src/shared/gateway-auth.ts diff --git a/src/agent/gateway-session.ts b/src/agent/gateway-session.ts index e8a1ef1b2..c1bb23d59 100644 --- a/src/agent/gateway-session.ts +++ b/src/agent/gateway-session.ts @@ -15,21 +15,14 @@ import { checkLlmGatewayHealth } from '@shared/health-checks/endpoints'; import { ServiceHealthStatus } from '@shared/health-checks/types'; import { IS_PRODUCTION_BUILD, runtimeEnv } from '@env'; import type { CloudRegion } from '@utils/types'; +import { isTrustedGatewayUrl, type GatewayAuth } from '@shared/gateway-auth'; -export interface GatewayAuth { - /** Base URL for model calls (no `/v1`; transports append their route). */ - gatewayUrl: string; - /** Gateway bearer, minted normally or supplied directly by CI. */ - token: string; - /** Team verified by the mint, or explicitly supplied for CI attribution. */ - teamId?: number; - /** - * Instant past which a 401 on this bearer is age rather than a bad - * credential: the cache re-mints past it, and a session still holding the - * old bearer may re-mint once. Before it the mint has to be trusted. - */ - refreshAtMs: number; -} +export { + buildWizardPropertiesBlob, + isPastRefresh, + isTrustedGatewayUrl, + type GatewayAuth, +} from '@shared/gateway-auth'; interface CachedAuth { key: string; @@ -191,54 +184,6 @@ export function resetGatewaySession(): void { ciAuth = null; } -/** Whether a 401 on this bearer may be age (past its refresh instant) rather than a bad credential. */ -export function isPastRefresh(auth: GatewayAuth, now = Date.now()): boolean { - return now >= auth.refreshAtMs; -} - -/** - * Whether a server-supplied origin may receive a bearer and prompt content: - * https (loopback excepted), and either a current cloud gateway or the host the run - * authenticated against. - */ -export function isTrustedGatewayUrl(value: string, apiHost: string): boolean { - let url: URL; - try { - url = new URL(value); - } catch { - return false; - } - // Consumers append routes to this value, so anything beyond an origin - // (path, query, fragment, userinfo) would build a malformed endpoint. - if ( - url.pathname !== '/' || - url.search || - url.hash || - url.username || - url.password - ) { - return false; - } - const localhost = - url.hostname === 'localhost' || - url.hostname === '127.0.0.1' || - url.hostname === 'host.docker.internal'; - // Loopback is the dev gateway, and is the one case allowed over http. - if (localhost) return true; - if (url.protocol !== 'https:') return false; - if (url.hostname.endsWith('.posthog.com')) { - return ( - url.origin === 'https://ai-gateway.us.posthog.com' || - url.origin === 'https://ai-gateway.eu.posthog.com' - ); - } - try { - return url.hostname === new URL(apiHost).hostname; - } catch { - return false; - } -} - interface MintedToken { token: string; expiresAt: string; @@ -450,38 +395,3 @@ async function mintGatewayToken( ); } } - -/** - * The v2 run-metadata carrier: one JSON blob for the `X-PostHog-Properties` - * header. Plain keys only, since the gateway strips `$`-prefixed keys as reserved, - * so feature-flag variants land as `wizard_flag_` instead of the legacy - * `$feature/` (dashboards keying on `$feature/wizard-*` read the new key - * post-cutover). - */ -export function buildWizardPropertiesBlob( - wizardMetadata: Record, - wizardFlags: Record, - teamId?: number, -): string { - // The gateway pins `$ai_product` to `wizard:`, and rejects a legacy - // product override on a scoped token, so the unprefixed key every cost and - // error consumer reads is only present if this blob declares it. - const props: Record = { ai_product: 'wizard' }; - if (teamId !== undefined) props.team_id = teamId; - for (const [key, value] of Object.entries(wizardMetadata)) { - props[stripPropertyPrefix(key)] = value; - } - for (const [flagKey, variant] of Object.entries(wizardFlags)) { - if (!flagKey.toLowerCase().startsWith('wizard')) continue; - props[`wizard_flag_${flagKey.toLowerCase()}`] = variant; - } - return JSON.stringify(props); -} - -const LEGACY_PROPERTY_PREFIX = 'X-POSTHOG-PROPERTY-'; - -function stripPropertyPrefix(key: string): string { - return key.toUpperCase().startsWith(LEGACY_PROPERTY_PREFIX) - ? key.slice(LEGACY_PROPERTY_PREFIX.length).toLowerCase() - : key; -} diff --git a/src/shared/gateway-auth.ts b/src/shared/gateway-auth.ts new file mode 100644 index 000000000..f673c5eb2 --- /dev/null +++ b/src/shared/gateway-auth.ts @@ -0,0 +1,99 @@ +/** Gateway bearer types and checks shared by the agent and its hosts. */ + +export interface GatewayAuth { + /** Base URL for model calls (no `/v1`; transports append their route). */ + gatewayUrl: string; + /** Gateway bearer, minted normally or supplied directly by CI. */ + token: string; + /** Team verified by the mint, or explicitly supplied for CI attribution. */ + teamId?: number; + /** + * Instant past which a 401 on this bearer is age rather than a bad + * credential: the cache re-mints past it, and a session still holding the + * old bearer may re-mint once. Before it the mint has to be trusted. + */ + refreshAtMs: number; +} + +/** Whether a 401 on this bearer may be age (past its refresh instant) rather than a bad credential. */ +export function isPastRefresh(auth: GatewayAuth, now = Date.now()): boolean { + return now >= auth.refreshAtMs; +} + +/** + * Whether a server-supplied origin may receive a bearer and prompt content: + * https (loopback excepted), and either a current cloud gateway or the host the run + * authenticated against. + */ +export function isTrustedGatewayUrl(value: string, apiHost: string): boolean { + let url: URL; + try { + url = new URL(value); + } catch { + return false; + } + // Consumers append routes to this value, so anything beyond an origin + // (path, query, fragment, userinfo) would build a malformed endpoint. + if ( + url.pathname !== '/' || + url.search || + url.hash || + url.username || + url.password + ) { + return false; + } + const localhost = + url.hostname === 'localhost' || + url.hostname === '127.0.0.1' || + url.hostname === 'host.docker.internal'; + // Loopback is the dev gateway, and is the one case allowed over http. + if (localhost) return true; + if (url.protocol !== 'https:') return false; + if (url.hostname.endsWith('.posthog.com')) { + return ( + url.origin === 'https://ai-gateway.us.posthog.com' || + url.origin === 'https://ai-gateway.eu.posthog.com' + ); + } + try { + return url.hostname === new URL(apiHost).hostname; + } catch { + return false; + } +} + +/** + * The v2 run-metadata carrier: one JSON blob for the `X-PostHog-Properties` + * header. Plain keys only, since the gateway strips `$`-prefixed keys as reserved, + * so feature-flag variants land as `wizard_flag_` instead of the legacy + * `$feature/` (dashboards keying on `$feature/wizard-*` read the new key + * post-cutover). + */ +export function buildWizardPropertiesBlob( + wizardMetadata: Record, + wizardFlags: Record, + teamId?: number, +): string { + // The gateway pins `$ai_product` to `wizard:`, and rejects a legacy + // product override on a scoped token, so the unprefixed key every cost and + // error consumer reads is only present if this blob declares it. + const props: Record = { ai_product: 'wizard' }; + if (teamId !== undefined) props.team_id = teamId; + for (const [key, value] of Object.entries(wizardMetadata)) { + props[stripPropertyPrefix(key)] = value; + } + for (const [flagKey, variant] of Object.entries(wizardFlags)) { + if (!flagKey.toLowerCase().startsWith('wizard')) continue; + props[`wizard_flag_${flagKey.toLowerCase()}`] = variant; + } + return JSON.stringify(props); +} + +const LEGACY_PROPERTY_PREFIX = 'X-POSTHOG-PROPERTY-'; + +function stripPropertyPrefix(key: string): string { + return key.toUpperCase().startsWith(LEGACY_PROPERTY_PREFIX) + ? key.slice(LEGACY_PROPERTY_PREFIX.length).toLowerCase() + : key; +} From 1eb4fe5836625bf4c9d1b774408495ad09d190b1 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:53:54 -0400 Subject: [PATCH 50/61] refactor(shared): move the JSON file watcher into shared src/lib/file-watcher.ts moves unchanged to src/shared/file-watcher.ts. The audit ledger watcher, the event-plan watcher and the TUI hook import it from @shared, which drops two programs-to-legacy rows from the architecture allowlist. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/__tests__/architecture/known-violations.json | 2 -- src/programs/audit/ledger-watcher.ts | 2 +- src/programs/task-stream/event-plan-watcher.ts | 2 +- src/{lib => shared}/file-watcher.ts | 0 src/ui/tui/hooks/file-watcher.ts | 12 +++++++++--- 5 files changed, 11 insertions(+), 7 deletions(-) rename src/{lib => shared}/file-watcher.ts (100%) diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 0d110b9d3..c91511369 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -42,7 +42,6 @@ "src/programs/ai-opt-in-gate.ts -> src/lib/wizard-session.ts", "src/programs/audit/index.ts -> src/lib/headless-mode.ts", "src/programs/audit/index.ts -> src/lib/wizard-session.ts", - "src/programs/audit/ledger-watcher.ts -> src/lib/file-watcher.ts", "src/programs/audit/ledger-watcher.ts -> src/ui/index.ts", "src/programs/audit/types.ts -> src/lib/wizard-session.ts", "src/programs/authenticate.ts -> src/lib/wizard-session.ts", @@ -111,7 +110,6 @@ "src/programs/shared/posthog-cli-preinstall.ts -> src/steps/install-cli-steering/index.ts", "src/programs/shared/posthog-cli-preinstall.ts -> src/ui/index.ts", "src/programs/task-stream/destinations/posthog.ts -> src/lib/wizard-session.ts", - "src/programs/task-stream/event-plan-watcher.ts -> src/lib/file-watcher.ts", "src/programs/task-stream/event-plan-watcher.ts -> src/ui/tui/store.ts", "src/programs/task-stream/task-stream-push.ts -> src/lib/wizard-session.ts", "src/programs/task-stream/task-stream-push.ts -> src/ui/tui/store.ts", diff --git a/src/programs/audit/ledger-watcher.ts b/src/programs/audit/ledger-watcher.ts index f7a312543..cf0c0f441 100644 --- a/src/programs/audit/ledger-watcher.ts +++ b/src/programs/audit/ledger-watcher.ts @@ -10,7 +10,7 @@ import { startFileWatcher, type FileWatcherHandle, type FileWatcherOptions, -} from '@lib/file-watcher'; +} from '@shared/file-watcher'; import { logToFile } from '@utils/debug'; import { AUDIT_CHECKS_KEY, coerceAuditChecks } from './types.js'; diff --git a/src/programs/task-stream/event-plan-watcher.ts b/src/programs/task-stream/event-plan-watcher.ts index d04cee77c..7fadb0118 100644 --- a/src/programs/task-stream/event-plan-watcher.ts +++ b/src/programs/task-stream/event-plan-watcher.ts @@ -3,7 +3,7 @@ import { startFileWatcher, type FileWatcherHandle, type FileWatcherOptions, -} from '@lib/file-watcher'; +} from '@shared/file-watcher'; const MAX_EVENT_PLAN_FILE_BYTES = 256 * 1024; const MAX_EVENT_COUNT = 50; diff --git a/src/lib/file-watcher.ts b/src/shared/file-watcher.ts similarity index 100% rename from src/lib/file-watcher.ts rename to src/shared/file-watcher.ts diff --git a/src/ui/tui/hooks/file-watcher.ts b/src/ui/tui/hooks/file-watcher.ts index 5dd272aa0..974466508 100644 --- a/src/ui/tui/hooks/file-watcher.ts +++ b/src/ui/tui/hooks/file-watcher.ts @@ -1,8 +1,14 @@ import { useEffect } from 'react'; -import { startFileWatcher, type FileWatcherOptions } from '@lib/file-watcher'; +import { + startFileWatcher, + type FileWatcherOptions, +} from '@shared/file-watcher'; -export { startFileWatcher } from '@lib/file-watcher'; -export type { FileWatcherHandle, FileWatcherOptions } from '@lib/file-watcher'; +export { startFileWatcher } from '@shared/file-watcher'; +export type { + FileWatcherHandle, + FileWatcherOptions, +} from '@shared/file-watcher'; /** React hook wrapping `startFileWatcher`. Starts on mount, stops on unmount * or when `path` changes. `onUpdate` and `options` are captured at mount From 0eb5f7381376b58356b990ce763f8bbf0968520e Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:55:11 -0400 Subject: [PATCH 51/61] refactor(shared): move the self-driving pricing copy into shared The pricing constants move unchanged from the self-driving deck into shared/self-driving-pricing.ts. The deck's pricing.ts re-exports them, so its importers keep their paths. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/shared/self-driving-pricing.ts | 30 +++++++++++++++++++ src/ui/tui/decks/self-driving/pricing.ts | 37 +++++------------------- 2 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 src/shared/self-driving-pricing.ts diff --git a/src/shared/self-driving-pricing.ts b/src/shared/self-driving-pricing.ts new file mode 100644 index 000000000..001c0f20f --- /dev/null +++ b/src/shared/self-driving-pricing.ts @@ -0,0 +1,30 @@ +/** + * Self-driving pricing, in one place. It's stated at six points across the run (intro, tips, + * learn deck, outro), and a price quoted six ways is a price the reader stops trusting. + * + * Careful with the word "free". The tracking this wizard installs bills as ordinary PostHog + * usage, and scouts are heading for usage-based pricing too — so anything called free here is + * a promise with an expiry date. State what the PR costs and leave the rest to usage-based + * billing, which stays true either side of that change. + */ + +/** Flat USD charge per report that ships a pull request. */ +export const PRICE_PER_PR_USD = 15; + +/** One line, for a screen that only has room to say what it costs. */ +export const PRICING_SHORT = `Agents charge a flat $${PRICE_PER_PR_USD} per pull request they ship.`; + +/** The same fact at length, plus the bit people are surprised by later. */ +export const PRICING_LONG = + `A report that ships a pull request costs a flat $${PRICE_PER_PR_USD}. ` + + `Everything else follows your usual PostHog usage-based pricing.`; + +/** + * Nothing caps the spend unless you set it, which is worth saying out loud. Checked against + * billing: `custom_limits_map` starts empty and a product with no custom limit resolves to + * `usage_limit: None`, i.e. uncapped. A free plan is capped by its own allocation instead, + * hence the qualifier. + */ +export const NO_DEFAULT_LIMIT = + 'A paid plan has no monthly cap until you set one. Add a PR limit in your inbox sidebar, ' + + 'and agents pause when they hit it.'; diff --git a/src/ui/tui/decks/self-driving/pricing.ts b/src/ui/tui/decks/self-driving/pricing.ts index 001c0f20f..237c09860 100644 --- a/src/ui/tui/decks/self-driving/pricing.ts +++ b/src/ui/tui/decks/self-driving/pricing.ts @@ -1,30 +1,7 @@ -/** - * Self-driving pricing, in one place. It's stated at six points across the run (intro, tips, - * learn deck, outro), and a price quoted six ways is a price the reader stops trusting. - * - * Careful with the word "free". The tracking this wizard installs bills as ordinary PostHog - * usage, and scouts are heading for usage-based pricing too — so anything called free here is - * a promise with an expiry date. State what the PR costs and leave the rest to usage-based - * billing, which stays true either side of that change. - */ - -/** Flat USD charge per report that ships a pull request. */ -export const PRICE_PER_PR_USD = 15; - -/** One line, for a screen that only has room to say what it costs. */ -export const PRICING_SHORT = `Agents charge a flat $${PRICE_PER_PR_USD} per pull request they ship.`; - -/** The same fact at length, plus the bit people are surprised by later. */ -export const PRICING_LONG = - `A report that ships a pull request costs a flat $${PRICE_PER_PR_USD}. ` + - `Everything else follows your usual PostHog usage-based pricing.`; - -/** - * Nothing caps the spend unless you set it, which is worth saying out loud. Checked against - * billing: `custom_limits_map` starts empty and a product with no custom limit resolves to - * `usage_limit: None`, i.e. uncapped. A free plan is capped by its own allocation instead, - * hence the qualifier. - */ -export const NO_DEFAULT_LIMIT = - 'A paid plan has no monthly cap until you set one. Add a PR limit in your inbox sidebar, ' + - 'and agents pause when they hit it.'; +/** UI compatibility entry for the self-driving pricing copy. */ +export { + PRICE_PER_PR_USD, + PRICING_SHORT, + PRICING_LONG, + NO_DEFAULT_LIMIT, +} from '@shared/self-driving-pricing'; From aed54993cee3b802f31a74bd1fe78f041398c968 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 10:56:37 -0400 Subject: [PATCH 52/61] refactor(agent): move the wizard-tools names into tool-names SERVER_NAME and WIZARD_TOOL_NAMES move unchanged from tools/tools.ts into tools/tool-names.ts, a data-only module. tools.ts re-exports both, so every importer keeps its path. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/tools/tool-names.ts | 24 ++++++++++++++++++++++++ src/agent/tools/tools.ts | 23 +---------------------- 2 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 src/agent/tools/tool-names.ts diff --git a/src/agent/tools/tool-names.ts b/src/agent/tools/tool-names.ts new file mode 100644 index 000000000..40853267d --- /dev/null +++ b/src/agent/tools/tool-names.ts @@ -0,0 +1,24 @@ +/** Tool ids programs name in allowedTools and disallowedTools. Data only. */ + +export const SERVER_NAME = 'wizard-tools'; + +/** Tool names exposed by the wizard-tools server, keyed for selective use. */ +// SDK expects MCP tool names in allowedTools/disallowedTools to be the +// fully-qualified `mcp____` form (sdk.d.ts: "Fully-qualified +// MCP tool name, e.g. mcp__server__tool_name."). The colon form silently +// fails to match, which made every program's `disallowedTools` entry a no-op. +export const WIZARD_TOOL_NAMES = { + checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`, + setEnvValues: `mcp__${SERVER_NAME}__set_env_values`, + detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`, + loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`, + installSkill: `mcp__${SERVER_NAME}__install_skill`, + auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`, + auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`, + auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`, + wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`, + publishHandoff: `mcp__${SERVER_NAME}__publish_handoff`, + enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`, + completeTask: `mcp__${SERVER_NAME}__complete_task`, + readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`, +} as const; diff --git a/src/agent/tools/tools.ts b/src/agent/tools/tools.ts index 6751232a7..0ab9a6283 100644 --- a/src/agent/tools/tools.ts +++ b/src/agent/tools/tools.ts @@ -1156,28 +1156,7 @@ export function appendAuditChecksToLedger( return { ok: true, added: additions.length }; } -export const SERVER_NAME = 'wizard-tools'; - -/** Tool names exposed by the wizard-tools server, keyed for selective use. */ -// SDK expects MCP tool names in allowedTools/disallowedTools to be the -// fully-qualified `mcp____` form (sdk.d.ts: "Fully-qualified -// MCP tool name, e.g. mcp__server__tool_name."). The colon form silently -// fails to match, which made every program's `disallowedTools` entry a no-op. -export const WIZARD_TOOL_NAMES = { - checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`, - setEnvValues: `mcp__${SERVER_NAME}__set_env_values`, - detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`, - loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`, - installSkill: `mcp__${SERVER_NAME}__install_skill`, - auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`, - auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`, - auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`, - wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`, - publishHandoff: `mcp__${SERVER_NAME}__publish_handoff`, - enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`, - completeTask: `mcp__${SERVER_NAME}__complete_task`, - readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`, -} as const; +export { SERVER_NAME, WIZARD_TOOL_NAMES } from './tool-names'; // --------------------------------------------------------------------------- // Test-only exports From 32151373834132ec5b6bc973b6d7e311884893a5 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 11:00:54 -0400 Subject: [PATCH 53/61] refactor(programs): move program run data into run and abort-case modules Eight programs keep their run definitions, skill options and abort cases in their index or detect files. That data now lives in run.ts, abort-cases.ts and agent-skill/run-definition.ts, and each index imports it. The data is unchanged: every exported config, prompt and abort case resolves to the same values as before. The old modules re-export SkillProgramOptions and each abort-case list, so importers keep their paths. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/programs/agent-skill/index.ts | 45 ++------------ src/programs/agent-skill/run-definition.ts | 43 +++++++++++++ src/programs/ai-observability/index.ts | 38 +----------- src/programs/ai-observability/run.ts | 39 ++++++++++++ src/programs/mcp-analytics/index.ts | 62 +------------------ src/programs/mcp-analytics/run.ts | 62 +++++++++++++++++++ src/programs/metrics/index.ts | 45 +------------- src/programs/metrics/run.ts | 44 +++++++++++++ src/programs/migration/index.ts | 44 ++----------- src/programs/migration/run.ts | 40 ++++++++++++ src/programs/replay-vision/index.ts | 47 +------------- src/programs/replay-vision/run.ts | 47 ++++++++++++++ src/programs/revenue-analytics/abort-cases.ts | 25 ++++++++ src/programs/revenue-analytics/detect.ts | 25 +------- src/programs/revenue-analytics/index.ts | 14 +---- src/programs/revenue-analytics/run.ts | 14 +++++ .../web-analytics-doctor/abort-cases.ts | 34 ++++++++++ src/programs/web-analytics-doctor/detect.ts | 34 +--------- src/programs/web-analytics-doctor/index.ts | 27 +------- src/programs/web-analytics-doctor/run.ts | 27 ++++++++ 20 files changed, 403 insertions(+), 353 deletions(-) create mode 100644 src/programs/agent-skill/run-definition.ts create mode 100644 src/programs/ai-observability/run.ts create mode 100644 src/programs/mcp-analytics/run.ts create mode 100644 src/programs/metrics/run.ts create mode 100644 src/programs/migration/run.ts create mode 100644 src/programs/replay-vision/run.ts create mode 100644 src/programs/revenue-analytics/abort-cases.ts create mode 100644 src/programs/revenue-analytics/run.ts create mode 100644 src/programs/web-analytics-doctor/abort-cases.ts create mode 100644 src/programs/web-analytics-doctor/run.ts diff --git a/src/programs/agent-skill/index.ts b/src/programs/agent-skill/index.ts index 75752731e..7632d30cf 100644 --- a/src/programs/agent-skill/index.ts +++ b/src/programs/agent-skill/index.ts @@ -20,36 +20,14 @@ */ import type { ProgramConfig } from '@programs/program-step'; -import type { AbortCase } from '@agent/types'; -import type { ProgramRun } from '@programs/program-run'; import { AGENT_SKILL_STEPS } from './steps.js'; import { getContentBlocks } from '../../ui/tui/decks/agent-skill/index.js'; +import { + skillRunDefinition, + type SkillProgramOptions, +} from './run-definition.js'; -export interface SkillProgramOptions { - /** Context-mill skill ID to install */ - skillId: string; - /** CLI subcommand name */ - command: string; - /** Unique flow key — must match a Program enum entry */ - id: string; - /** CLI description shown in --help */ - description: string; - /** Analytics integration label */ - integrationLabel: string; - /** Custom prompt instruction. Appended after default project prompt. */ - customPrompt?: string; - successMessage: string; - reportFile: string; - docsUrl: string; - spinnerMessage: string; - estimatedDurationMinutes: number; - /** Other program ids that must be satisfied first */ - requires?: string[]; - /** Override the default outro. Receives the same args as ProgramRun.buildOutroData. */ - buildOutroData?: ProgramRun['buildOutroData']; - /** Known `[ABORT] ` cases the skill can emit. */ - abortCases?: AbortCase[]; -} +export type { SkillProgramOptions } from './run-definition.js'; export function createSkillProgram(opts: SkillProgramOptions): ProgramConfig { return { @@ -60,18 +38,7 @@ export function createSkillProgram(opts: SkillProgramOptions): ProgramConfig { steps: AGENT_SKILL_STEPS, reportFile: opts.reportFile, getContentBlocks, - run: { - skillId: opts.skillId, - integrationLabel: opts.integrationLabel, - customPrompt: opts.customPrompt ? () => opts.customPrompt! : undefined, - successMessage: opts.successMessage, - reportFile: opts.reportFile, - docsUrl: opts.docsUrl, - spinnerMessage: opts.spinnerMessage, - estimatedDurationMinutes: opts.estimatedDurationMinutes, - buildOutroData: opts.buildOutroData, - abortCases: opts.abortCases, - }, + run: skillRunDefinition(opts), requires: opts.requires, }; } diff --git a/src/programs/agent-skill/run-definition.ts b/src/programs/agent-skill/run-definition.ts new file mode 100644 index 000000000..6a13a9570 --- /dev/null +++ b/src/programs/agent-skill/run-definition.ts @@ -0,0 +1,43 @@ +import type { AbortCase } from '@agent/types'; +import type { ProgramRun } from '@programs/program-run'; + +export interface SkillProgramOptions { + /** Context-mill skill ID to install */ + skillId: string; + /** CLI subcommand name */ + command: string; + /** Unique flow key — must match a Program enum entry */ + id: string; + /** CLI description shown in --help */ + description: string; + /** Analytics integration label */ + integrationLabel: string; + /** Custom prompt instruction. Appended after default project prompt. */ + customPrompt?: string; + successMessage: string; + reportFile: string; + docsUrl: string; + spinnerMessage: string; + estimatedDurationMinutes: number; + /** Other program ids that must be satisfied first */ + requires?: string[]; + /** Override the default outro. Receives the same args as ProgramRun.buildOutroData. */ + buildOutroData?: ProgramRun['buildOutroData']; + /** Known `[ABORT] ` cases the skill can emit. */ + abortCases?: AbortCase[]; +} + +export function skillRunDefinition(opts: SkillProgramOptions): ProgramRun { + return { + skillId: opts.skillId, + integrationLabel: opts.integrationLabel, + customPrompt: opts.customPrompt ? () => opts.customPrompt! : undefined, + successMessage: opts.successMessage, + reportFile: opts.reportFile, + docsUrl: opts.docsUrl, + spinnerMessage: opts.spinnerMessage, + estimatedDurationMinutes: opts.estimatedDurationMinutes, + buildOutroData: opts.buildOutroData, + abortCases: opts.abortCases, + }; +} diff --git a/src/programs/ai-observability/index.ts b/src/programs/ai-observability/index.ts index b71697d30..68b08030c 100644 --- a/src/programs/ai-observability/index.ts +++ b/src/programs/ai-observability/index.ts @@ -2,13 +2,12 @@ import type { ProgramConfig, ProgramStep } from '@programs/program-step'; import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; import { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; import { headlessOption, regionOption } from '@lib/headless-mode'; +import { AI_OBSERVABILITY_REPORT_FILE, AI_OBSERVABILITY_RUN } from './run.js'; const AI_OBSERVABILITY_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => step.id === 'intro' ? { ...step, screenId: 'ai-observability-intro' } : step, ); -const AI_OBSERVABILITY_REPORT_FILE = 'posthog-ai-observability-report.md'; - /** * `wizard ai-observability` — wrap the project's LLM client calls so they emit * `$ai_generation` events into AI Observability. @@ -27,38 +26,5 @@ export const aiObservabilityConfig: ProgramConfig = { steps: AI_OBSERVABILITY_STEPS, reportFile: AI_OBSERVABILITY_REPORT_FILE, getContentBlocks, - run: { - integrationLabel: 'ai-observability', - // No `skillId`: linear.ts skips its pre-install step (see the gate on - // `linear.ts:47`), so the agent must load the menu and install the right - // variant itself. The prompt below tells it how. - customPrompt: - () => `Instrument this project's LLM calls with PostHog AI Observability. - -This flow has no pre-installed skill — you install the right one yourself: - -1. Call \`load_skill_menu\` with \`category: "ai-observability"\`. The menu is - the source of truth: one variant per (LLM provider × language), plus a - \`manual-capture\` variant for projects with no vendor SDK. - -2. Scan the project manifest (\`package.json\`, \`pyproject.toml\`, - \`requirements.txt\`) for a vendor LLM SDK and pick the variant that matches - it — the language follows the manifest (\`package.json\` → Node, Python - tooling → Python). Multiple SDKs (e.g. LangChain wrapping OpenAI) → prefer - the higher abstraction. No vendor SDK → the \`manual-capture\` variant. - Genuinely ambiguous → \`wizard_ask\` with a multi-choice picker. - -3. Call \`install_skill\` with the picked variant id. Then follow that skill's - \`SKILL.md\` and references end-to-end. The skill itself will install - packages, wire OTel, set env vars, and describe verification. - -Make only additive changes — do not touch existing PostHog init, identify -calls, event capture, or dashboards. Those belong to other skills. The -final report is written to ./${AI_OBSERVABILITY_REPORT_FILE}.`, - successMessage: `AI Observability configured! View the report at ./${AI_OBSERVABILITY_REPORT_FILE}`, - reportFile: AI_OBSERVABILITY_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/ai-observability', - spinnerMessage: 'Setting up AI Observability...', - estimatedDurationMinutes: 5, - }, + run: AI_OBSERVABILITY_RUN, }; diff --git a/src/programs/ai-observability/run.ts b/src/programs/ai-observability/run.ts new file mode 100644 index 000000000..1e9510e42 --- /dev/null +++ b/src/programs/ai-observability/run.ts @@ -0,0 +1,39 @@ +import type { ProgramRun } from '@programs/program-run'; + +export const AI_OBSERVABILITY_REPORT_FILE = + 'posthog-ai-observability-report.md'; + +export const AI_OBSERVABILITY_RUN: ProgramRun = { + integrationLabel: 'ai-observability', + // No `skillId`: linear.ts skips its pre-install step (see the gate on + // `linear.ts:47`), so the agent must load the menu and install the right + // variant itself. The prompt below tells it how. + customPrompt: + () => `Instrument this project's LLM calls with PostHog AI Observability. + +This flow has no pre-installed skill — you install the right one yourself: + +1. Call \`load_skill_menu\` with \`category: "ai-observability"\`. The menu is + the source of truth: one variant per (LLM provider × language), plus a + \`manual-capture\` variant for projects with no vendor SDK. + +2. Scan the project manifest (\`package.json\`, \`pyproject.toml\`, + \`requirements.txt\`) for a vendor LLM SDK and pick the variant that matches + it — the language follows the manifest (\`package.json\` → Node, Python + tooling → Python). Multiple SDKs (e.g. LangChain wrapping OpenAI) → prefer + the higher abstraction. No vendor SDK → the \`manual-capture\` variant. + Genuinely ambiguous → \`wizard_ask\` with a multi-choice picker. + +3. Call \`install_skill\` with the picked variant id. Then follow that skill's + \`SKILL.md\` and references end-to-end. The skill itself will install + packages, wire OTel, set env vars, and describe verification. + +Make only additive changes — do not touch existing PostHog init, identify +calls, event capture, or dashboards. Those belong to other skills. The +final report is written to ./${AI_OBSERVABILITY_REPORT_FILE}.`, + successMessage: `AI Observability configured! View the report at ./${AI_OBSERVABILITY_REPORT_FILE}`, + reportFile: AI_OBSERVABILITY_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/ai-observability', + spinnerMessage: 'Setting up AI Observability...', + estimatedDurationMinutes: 5, +}; diff --git a/src/programs/mcp-analytics/index.ts b/src/programs/mcp-analytics/index.ts index faa9afd1c..c8b4bba50 100644 --- a/src/programs/mcp-analytics/index.ts +++ b/src/programs/mcp-analytics/index.ts @@ -1,44 +1,7 @@ -import type { AbortCase } from '@agent/types'; -import { ErrorCodes } from '@shared/errors'; import { createSkillProgram } from '@programs/agent-skill/index'; +import { MCP_ANALYTICS_OPTIONS } from './run.js'; -const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; - -/** - * `[ABORT]` reasons the mcp-analytics skill emits when the project can't be - * instrumented. Kept in sync with the stop conditions in the skill's - * `description.md` (context-mill `context/skills/mcp-analytics`). - */ -export const MCP_ANALYTICS_ABORT_CASES: AbortCase[] = [ - { - match: /^unsupported language for mcp analytics$/i, - errorCode: ErrorCodes.DetectUnsupportedPlatform, - message: 'Unsupported language for MCP analytics', - body: - 'MCP analytics supports TypeScript/JavaScript (`@posthog/mcp`) and Python ' + - '(`posthog.mcp`, shipped inside the `posthog` package). This project ' + - "doesn't look like either, so there's nothing to instrument. " + - 'See https://posthog.com/docs/mcp-analytics for the supported setups.', - }, - { - match: /^no mcp server found$/i, - message: 'No MCP server found', - body: - 'This command instruments an existing MCP server with PostHog analytics, ' + - 'but no MCP server was found in this project. If you just want PostHog ' + - 'product analytics, run `npx @posthog/wizard` instead.', - }, - { - match: /^could not locate the server entry point$/i, - message: 'Could not locate the MCP server entry point', - body: - "This project has MCP signals, but the agent couldn't find where the " + - "server is constructed or requests are dispatched, so there's nowhere " + - 'safe to add instrumentation. See https://posthog.com/docs/mcp-analytics ' + - 'for the supported server styles, or point the wizard at the package ' + - "that defines the server if it's in a monorepo subdirectory.", - }, -]; +export { MCP_ANALYTICS_ABORT_CASES } from './run.js'; /** * `wizard mcp-analytics` — flat skill command. @@ -54,23 +17,4 @@ export const MCP_ANALYTICS_ABORT_CASES: AbortCase[] = [ * 'mcp-analytics'` from context-mill — a deliberate breaking change, done then, * not pre-emptively. */ -export const mcpAnalyticsConfig = createSkillProgram({ - skillId: 'mcp-analytics', - command: 'mcp-analytics', - id: 'mcp-analytics', - description: 'Add PostHog MCP Analytics to your MCP server', - integrationLabel: 'mcp-analytics', - customPrompt: - "Instrument this project's MCP server with PostHog MCP analytics. Run the " + - '`mcp-analytics` skill end-to-end: detect the server style, install ' + - '`@posthog/mcp` and `posthog-node`, wrap the server (or use `PostHogMCP` ' + - 'for a custom dispatcher), wire the project API key and host, and verify. ' + - 'Make only additive changes — do not alter tool behavior. The final report ' + - `is written to ./${MCP_ANALYTICS_REPORT_FILE}.`, - successMessage: `MCP analytics configured! View the report at ./${MCP_ANALYTICS_REPORT_FILE}`, - reportFile: MCP_ANALYTICS_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/mcp-analytics', - spinnerMessage: 'Setting up MCP analytics...', - estimatedDurationMinutes: 5, - abortCases: MCP_ANALYTICS_ABORT_CASES, -}); +export const mcpAnalyticsConfig = createSkillProgram(MCP_ANALYTICS_OPTIONS); diff --git a/src/programs/mcp-analytics/run.ts b/src/programs/mcp-analytics/run.ts new file mode 100644 index 000000000..690a31291 --- /dev/null +++ b/src/programs/mcp-analytics/run.ts @@ -0,0 +1,62 @@ +import type { AbortCase } from '@agent/types'; +import { ErrorCodes } from '@shared/errors'; +import type { SkillProgramOptions } from '@programs/agent-skill/run-definition'; + +const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; + +/** + * `[ABORT]` reasons the mcp-analytics skill emits when the project can't be + * instrumented. Kept in sync with the stop conditions in the skill's + * `description.md` (context-mill `context/skills/mcp-analytics`). + */ +export const MCP_ANALYTICS_ABORT_CASES: AbortCase[] = [ + { + match: /^unsupported language for mcp analytics$/i, + errorCode: ErrorCodes.DetectUnsupportedPlatform, + message: 'Unsupported language for MCP analytics', + body: + 'MCP analytics supports TypeScript/JavaScript (`@posthog/mcp`) and Python ' + + '(`posthog.mcp`, shipped inside the `posthog` package). This project ' + + "doesn't look like either, so there's nothing to instrument. " + + 'See https://posthog.com/docs/mcp-analytics for the supported setups.', + }, + { + match: /^no mcp server found$/i, + message: 'No MCP server found', + body: + 'This command instruments an existing MCP server with PostHog analytics, ' + + 'but no MCP server was found in this project. If you just want PostHog ' + + 'product analytics, run `npx @posthog/wizard` instead.', + }, + { + match: /^could not locate the server entry point$/i, + message: 'Could not locate the MCP server entry point', + body: + "This project has MCP signals, but the agent couldn't find where the " + + "server is constructed or requests are dispatched, so there's nowhere " + + 'safe to add instrumentation. See https://posthog.com/docs/mcp-analytics ' + + 'for the supported server styles, or point the wizard at the package ' + + "that defines the server if it's in a monorepo subdirectory.", + }, +]; + +export const MCP_ANALYTICS_OPTIONS: SkillProgramOptions = { + skillId: 'mcp-analytics', + command: 'mcp-analytics', + id: 'mcp-analytics', + description: 'Add PostHog MCP Analytics to your MCP server', + integrationLabel: 'mcp-analytics', + customPrompt: + "Instrument this project's MCP server with PostHog MCP analytics. Run the " + + '`mcp-analytics` skill end-to-end: detect the server style, install ' + + '`@posthog/mcp` and `posthog-node`, wrap the server (or use `PostHogMCP` ' + + 'for a custom dispatcher), wire the project API key and host, and verify. ' + + 'Make only additive changes — do not alter tool behavior. The final report ' + + `is written to ./${MCP_ANALYTICS_REPORT_FILE}.`, + successMessage: `MCP analytics configured! View the report at ./${MCP_ANALYTICS_REPORT_FILE}`, + reportFile: MCP_ANALYTICS_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/mcp-analytics', + spinnerMessage: 'Setting up MCP analytics...', + estimatedDurationMinutes: 5, + abortCases: MCP_ANALYTICS_ABORT_CASES, +}; diff --git a/src/programs/metrics/index.ts b/src/programs/metrics/index.ts index c96612ae3..3cce288e5 100644 --- a/src/programs/metrics/index.ts +++ b/src/programs/metrics/index.ts @@ -1,13 +1,12 @@ import type { ProgramConfig, ProgramStep } from '@programs/program-step'; import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; import { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; +import { METRICS_REPORT_FILE, METRICS_RUN } from './run.js'; const METRICS_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => step.id === 'intro' ? { ...step, screenId: 'metrics-intro' } : step, ); -const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; - /** * `wizard metrics` — instrument the project with PostHog application metrics * (`posthog.metrics` counters, gauges, and histograms). @@ -30,45 +29,5 @@ export const metricsConfig: ProgramConfig = { steps: METRICS_STEPS, reportFile: METRICS_REPORT_FILE, getContentBlocks, - run: { - integrationLabel: 'metrics', - // No `skillId`: the agent must load the menu and install the right - // variant itself. The prompt below tells it how. - customPrompt: - () => `Instrument this project with PostHog application metrics. - -This flow has no pre-installed skill — you install the right one yourself: - -1. Call \`load_skill_menu\` with \`category: "metrics"\`. The menu is the - source of truth: one variant per platform. - -2. Pick the variant that matches the project: - - Python tooling (\`pyproject.toml\`, \`requirements.txt\`, \`Pipfile\`) → - \`metrics-python\` (needs \`posthog\` >= 7.23.0) - - \`package.json\` with server-side Node code → \`metrics-nodejs\` - (needs \`posthog-node\` >= 5.43.0) - - \`package.json\` that is browser-only → \`metrics-javascript\` - (needs \`posthog-js\` >= 1.399.0) - - Kubernetes manifests / Helm charts and the user wants cluster-level - scraping → \`metrics-kubernetes\` - - Any other language → \`metrics-other\` (plain OTLP exporter) - A full-stack app (e.g. Next.js) usually wants the server variant — metrics - measure service work, not user actions. Genuinely ambiguous → - \`wizard_ask\` with a multi-choice picker. - -3. Call \`install_skill\` with the picked variant id. Then follow that skill's - \`SKILL.md\` and references end-to-end — it covers where to place metrics - (middleware, background jobs, external calls, business commit sites) and - the low-cardinality attribute rules. - -Make only additive changes — reuse an existing PostHog client by adding the -\`metrics\` config to it rather than constructing a second client, and do not -touch existing identify calls, event capture, or dashboards. The final report -is written to ./${METRICS_REPORT_FILE}.`, - successMessage: `Application metrics configured! View the report at ./${METRICS_REPORT_FILE}`, - reportFile: METRICS_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/metrics', - spinnerMessage: 'Setting up application metrics...', - estimatedDurationMinutes: 5, - }, + run: METRICS_RUN, }; diff --git a/src/programs/metrics/run.ts b/src/programs/metrics/run.ts new file mode 100644 index 000000000..295cc2455 --- /dev/null +++ b/src/programs/metrics/run.ts @@ -0,0 +1,44 @@ +import type { ProgramRun } from '@programs/program-run'; + +export const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; + +export const METRICS_RUN: ProgramRun = { + integrationLabel: 'metrics', + // No `skillId`: the agent must load the menu and install the right + // variant itself. The prompt below tells it how. + customPrompt: () => `Instrument this project with PostHog application metrics. + +This flow has no pre-installed skill — you install the right one yourself: + +1. Call \`load_skill_menu\` with \`category: "metrics"\`. The menu is the + source of truth: one variant per platform. + +2. Pick the variant that matches the project: + - Python tooling (\`pyproject.toml\`, \`requirements.txt\`, \`Pipfile\`) → + \`metrics-python\` (needs \`posthog\` >= 7.23.0) + - \`package.json\` with server-side Node code → \`metrics-nodejs\` + (needs \`posthog-node\` >= 5.43.0) + - \`package.json\` that is browser-only → \`metrics-javascript\` + (needs \`posthog-js\` >= 1.399.0) + - Kubernetes manifests / Helm charts and the user wants cluster-level + scraping → \`metrics-kubernetes\` + - Any other language → \`metrics-other\` (plain OTLP exporter) + A full-stack app (e.g. Next.js) usually wants the server variant — metrics + measure service work, not user actions. Genuinely ambiguous → + \`wizard_ask\` with a multi-choice picker. + +3. Call \`install_skill\` with the picked variant id. Then follow that skill's + \`SKILL.md\` and references end-to-end — it covers where to place metrics + (middleware, background jobs, external calls, business commit sites) and + the low-cardinality attribute rules. + +Make only additive changes — reuse an existing PostHog client by adding the +\`metrics\` config to it rather than constructing a second client, and do not +touch existing identify calls, event capture, or dashboards. The final report +is written to ./${METRICS_REPORT_FILE}.`, + successMessage: `Application metrics configured! View the report at ./${METRICS_REPORT_FILE}`, + reportFile: METRICS_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/metrics', + spinnerMessage: 'Setting up application metrics...', + estimatedDurationMinutes: 5, +}; diff --git a/src/programs/migration/index.ts b/src/programs/migration/index.ts index 907160f6d..81f0db084 100644 --- a/src/programs/migration/index.ts +++ b/src/programs/migration/index.ts @@ -1,28 +1,12 @@ import type { ProgramConfig } from '@programs/program-step'; -import type { AbortCase } from '@agent/types'; import { WIZARD_TOOL_NAMES } from '@agent'; import { MIGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from '../../ui/tui/decks/migration/index.js'; - -const MIGRATION_REPORT_FILE = 'migration-report.md'; - -const MIGRATION_ABORT_CASES: AbortCase[] = [ - { - match: /^no source-sdk calls found$/i, - message: 'No source-SDK calls found', - body: - 'The migration needs an existing third-party SDK to migrate from. No ' + - 'calls to the source SDK appear anywhere in this project. If you ' + - "haven't installed PostHog yet, you don't need this command — run " + - '`npx @posthog/wizard@latest` to add PostHog from scratch.', - }, -]; - -// Default skill id when nothing else picks one. The `wizard migrate ` -// subcommands override this via skillCommandFactory using each manifest -// entry's skillId, so this default only kicks in for legacy callers (e.g. -// programmatic uses of migrationConfig directly). -const DEFAULT_MIGRATE_SKILL_ID = 'migrate-statsig'; +import { + DEFAULT_MIGRATE_SKILL_ID, + MIGRATION_REPORT_FILE, + MIGRATION_RUN, +} from './run.js'; export const migrationConfig: ProgramConfig = { command: 'migrate', @@ -34,23 +18,7 @@ export const migrationConfig: ProgramConfig = { getContentBlocks, allowedTools: ['Agent'], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], - run: { - skillId: DEFAULT_MIGRATE_SKILL_ID, - integrationLabel: 'migration', - customPrompt: () => - 'Migrate this project from its existing third-party analytics, ' + - 'feature-flag, and observability tools to PostHog. Run the `migrate` ' + - 'skill end-to-end: follow the step chain starting at ' + - 'references/1-presence.md. Only replace existing source-SDK call sites ' + - 'with PostHog equivalents — make zero unrelated changes and no ' + - `net-new instrumentation. The final report is written to ./${MIGRATION_REPORT_FILE}.`, - successMessage: `Migration complete! View the report at ./${MIGRATION_REPORT_FILE}`, - reportFile: MIGRATION_REPORT_FILE, - docsUrl: '', - spinnerMessage: 'Migrating to PostHog...', - estimatedDurationMinutes: 8, - abortCases: MIGRATION_ABORT_CASES, - }, + run: MIGRATION_RUN, requires: ['posthog-integration'], }; diff --git a/src/programs/migration/run.ts b/src/programs/migration/run.ts new file mode 100644 index 000000000..25a28f14d --- /dev/null +++ b/src/programs/migration/run.ts @@ -0,0 +1,40 @@ +import type { AbortCase } from '@agent/types'; +import type { ProgramRun } from '@programs/program-run'; + +export const MIGRATION_REPORT_FILE = 'migration-report.md'; + +const MIGRATION_ABORT_CASES: AbortCase[] = [ + { + match: /^no source-sdk calls found$/i, + message: 'No source-SDK calls found', + body: + 'The migration needs an existing third-party SDK to migrate from. No ' + + 'calls to the source SDK appear anywhere in this project. If you ' + + "haven't installed PostHog yet, you don't need this command — run " + + '`npx @posthog/wizard@latest` to add PostHog from scratch.', + }, +]; + +// Default skill id when nothing else picks one. The `wizard migrate ` +// subcommands override this via skillCommandFactory using each manifest +// entry's skillId, so this default only kicks in for legacy callers (e.g. +// programmatic uses of migrationConfig directly). +export const DEFAULT_MIGRATE_SKILL_ID = 'migrate-statsig'; + +export const MIGRATION_RUN: ProgramRun = { + skillId: DEFAULT_MIGRATE_SKILL_ID, + integrationLabel: 'migration', + customPrompt: () => + 'Migrate this project from its existing third-party analytics, ' + + 'feature-flag, and observability tools to PostHog. Run the `migrate` ' + + 'skill end-to-end: follow the step chain starting at ' + + 'references/1-presence.md. Only replace existing source-SDK call sites ' + + 'with PostHog equivalents — make zero unrelated changes and no ' + + `net-new instrumentation. The final report is written to ./${MIGRATION_REPORT_FILE}.`, + successMessage: `Migration complete! View the report at ./${MIGRATION_REPORT_FILE}`, + reportFile: MIGRATION_REPORT_FILE, + docsUrl: '', + spinnerMessage: 'Migrating to PostHog...', + estimatedDurationMinutes: 8, + abortCases: MIGRATION_ABORT_CASES, +}; diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index 314bfc9a4..2c6561d97 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -1,4 +1,3 @@ -import type { AbortCase } from '@agent/types'; import { Integration } from '@shared/constants'; import { detectFramework, @@ -18,8 +17,9 @@ import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; +import { REPLAY_VISION_OPTIONS } from './run.js'; -const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; +export { REPLAY_VISION_ABORT_CASES } from './run.js'; /** * The platforms session replay can actually record on. Replay vision watches @@ -77,22 +77,6 @@ async function abortUnsupportedPlatform( }); } -/** - * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. - * Kept in sync with the stop conditions in the skill's `description.md` - * (context-mill `context/skills/replay-vision`). - */ -export const REPLAY_VISION_ABORT_CASES: AbortCase[] = [ - { - match: /^replay vision not available for this project$/i, - message: 'Replay vision is not available for this project', - body: - 'Every Replay vision scanner endpoint reported that the feature is not ' + - 'available here yet. Session replay setup done so far is kept. See ' + - 'https://posthog.com/docs/replay-vision for availability.', - }, -]; - /** * Framework detection ahead of the run, exactly like the default integration * program. The orchestrator requires it: `session.skillId` must hold the @@ -119,32 +103,7 @@ const DETECT_STEP: ProgramStep = { }, }; -const base = createSkillProgram({ - // The menu ids this skill `-`, and context-mill's - // `replay-vision/config.yaml` declares a single variant, `setup`. The bare - // `replay-vision` id does not exist — the orchestrator never installs this - // (it resolves per-task mini-skills instead), but the linear path does, and - // aborts `skill-not-found` on a miss. - skillId: 'replay-vision-setup', - command: 'replay-vision', - id: 'replay-vision', - description: 'Set up PostHog Replay Vision scanners for your product', - integrationLabel: 'replay-vision', - customPrompt: - 'Set up PostHog Replay vision. Run the `replay-vision` skill end-to-end: ' + - 'make sure session replay is recording (server-side enable plus a ' + - 'posthog-js init check), then create the vision scanners the skill ' + - "defines, scoped to this product's key flows read out of the repo. If " + - 'PostHog is not integrated yet, install and initialize the SDK first as ' + - 'the skill instructs — do not abort. The final report is written to ' + - `./${REPLAY_VISION_REPORT_FILE}.`, - successMessage: `Replay vision configured! View the report at ./${REPLAY_VISION_REPORT_FILE}`, - reportFile: REPLAY_VISION_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/replay-vision', - spinnerMessage: 'Setting up Replay vision...', - estimatedDurationMinutes: 6, - abortCases: REPLAY_VISION_ABORT_CASES, -}); +const base = createSkillProgram(REPLAY_VISION_OPTIONS); /** * `wizard replay-vision` — flat skill command on the orchestrator sequence. diff --git a/src/programs/replay-vision/run.ts b/src/programs/replay-vision/run.ts new file mode 100644 index 000000000..b8eaca446 --- /dev/null +++ b/src/programs/replay-vision/run.ts @@ -0,0 +1,47 @@ +import type { AbortCase } from '@agent/types'; +import type { SkillProgramOptions } from '@programs/agent-skill/run-definition'; + +const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; + +/** + * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. + * Kept in sync with the stop conditions in the skill's `description.md` + * (context-mill `context/skills/replay-vision`). + */ +export const REPLAY_VISION_ABORT_CASES: AbortCase[] = [ + { + match: /^replay vision not available for this project$/i, + message: 'Replay vision is not available for this project', + body: + 'Every Replay vision scanner endpoint reported that the feature is not ' + + 'available here yet. Session replay setup done so far is kept. See ' + + 'https://posthog.com/docs/replay-vision for availability.', + }, +]; + +export const REPLAY_VISION_OPTIONS: SkillProgramOptions = { + // The menu ids this skill `-`, and context-mill's + // `replay-vision/config.yaml` declares a single variant, `setup`. The bare + // `replay-vision` id does not exist — the orchestrator never installs this + // (it resolves per-task mini-skills instead), but the linear path does, and + // aborts `skill-not-found` on a miss. + skillId: 'replay-vision-setup', + command: 'replay-vision', + id: 'replay-vision', + description: 'Set up PostHog Replay Vision scanners for your product', + integrationLabel: 'replay-vision', + customPrompt: + 'Set up PostHog Replay vision. Run the `replay-vision` skill end-to-end: ' + + 'make sure session replay is recording (server-side enable plus a ' + + 'posthog-js init check), then create the vision scanners the skill ' + + "defines, scoped to this product's key flows read out of the repo. If " + + 'PostHog is not integrated yet, install and initialize the SDK first as ' + + 'the skill instructs — do not abort. The final report is written to ' + + `./${REPLAY_VISION_REPORT_FILE}.`, + successMessage: `Replay vision configured! View the report at ./${REPLAY_VISION_REPORT_FILE}`, + reportFile: REPLAY_VISION_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/replay-vision', + spinnerMessage: 'Setting up Replay vision...', + estimatedDurationMinutes: 6, + abortCases: REPLAY_VISION_ABORT_CASES, +}; diff --git a/src/programs/revenue-analytics/abort-cases.ts b/src/programs/revenue-analytics/abort-cases.ts new file mode 100644 index 000000000..abb917a32 --- /dev/null +++ b/src/programs/revenue-analytics/abort-cases.ts @@ -0,0 +1,25 @@ +import type { AbortCase } from '@agent/types'; + +/** `[ABORT] ` cases the revenue analytics skill can emit. */ +export const REVENUE_ABORT_CASES: AbortCase[] = [ + { + // Skill emits: [ABORT] Could not find a PostHog distinct_id + match: /^could not find a posthog distinct_id$/i, + message: 'Could not find a PostHog distinct_id', + body: + 'The agent could not find PostHog distinct_id usage in your codebase. ' + + 'Your users must be identified in PostHog before they can be tagged in Stripe. ' + + 'Please identify your users and try again.', + docsUrl: 'https://posthog.com/docs/product-analytics/identify', + }, + { + // Skill emits: [ABORT] Could not find a Stripe integration + match: /^could not find a stripe integration$/i, + message: 'Could not find a Stripe integration', + body: + 'The Wizard could not find an existing Stripe customer, charge, ' + + 'subscription, or other Stripe operations. Please run the Revenue ' + + 'Analytics Wizard on a project with an existing Stripe integration.', + docsUrl: 'https://posthog.com/docs/revenue-analytics', + }, +]; diff --git a/src/programs/revenue-analytics/detect.ts b/src/programs/revenue-analytics/detect.ts index b81090ffd..380a40628 100644 --- a/src/programs/revenue-analytics/detect.ts +++ b/src/programs/revenue-analytics/detect.ts @@ -7,7 +7,6 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@agent/types'; import { findPackageJsons } from '@programs/shared/package-scanning'; export { @@ -32,29 +31,7 @@ export type RevenueDetectError = | { kind: 'missing-posthog'; foundStripe: string[] } | { kind: 'missing-stripe'; foundPosthog: string[] }; -/** `[ABORT] ` cases the revenue analytics skill can emit. */ -export const REVENUE_ABORT_CASES: AbortCase[] = [ - { - // Skill emits: [ABORT] Could not find a PostHog distinct_id - match: /^could not find a posthog distinct_id$/i, - message: 'Could not find a PostHog distinct_id', - body: - 'The agent could not find PostHog distinct_id usage in your codebase. ' + - 'Your users must be identified in PostHog before they can be tagged in Stripe. ' + - 'Please identify your users and try again.', - docsUrl: 'https://posthog.com/docs/product-analytics/identify', - }, - { - // Skill emits: [ABORT] Could not find a Stripe integration - match: /^could not find a stripe integration$/i, - message: 'Could not find a Stripe integration', - body: - 'The Wizard could not find an existing Stripe customer, charge, ' + - 'subscription, or other Stripe operations. Please run the Revenue ' + - 'Analytics Wizard on a project with an existing Stripe integration.', - docsUrl: 'https://posthog.com/docs/revenue-analytics', - }, -]; +export { REVENUE_ABORT_CASES } from './abort-cases.js'; /** * Scan `session.installDir` for PostHog + Stripe SDKs. Writes detection diff --git a/src/programs/revenue-analytics/index.ts b/src/programs/revenue-analytics/index.ts index 4fae357ac..ac321880b 100644 --- a/src/programs/revenue-analytics/index.ts +++ b/src/programs/revenue-analytics/index.ts @@ -1,8 +1,8 @@ import type { ProgramConfig } from '@programs/program-step'; import { WIZARD_TOOL_NAMES } from '@agent'; import { REVENUE_ANALYTICS_PROGRAM } from './steps.js'; -import { REVENUE_ABORT_CASES } from './detect.js'; import { getContentBlocks } from '../../ui/tui/decks/revenue-analytics/index.js'; +import { REVENUE_ANALYTICS_RUN } from './run.js'; export const revenueAnalyticsConfig: ProgramConfig = { command: 'revenue-analytics', @@ -13,17 +13,7 @@ export const revenueAnalyticsConfig: ProgramConfig = { getContentBlocks, allowedTools: ['Agent'], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], - run: { - skillId: 'revenue-analytics-setup', - integrationLabel: 'revenue-analytics-setup', - customPrompt: () => 'Set up revenue analytics for this project.', - successMessage: 'Revenue analytics configured!', - reportFile: 'posthog-revenue-report.md', - docsUrl: 'https://posthog.com/docs/revenue-analytics', - spinnerMessage: 'Setting up revenue analytics...', - estimatedDurationMinutes: 5, - abortCases: REVENUE_ABORT_CASES, - }, + run: REVENUE_ANALYTICS_RUN, requires: ['posthog-integration'], }; diff --git a/src/programs/revenue-analytics/run.ts b/src/programs/revenue-analytics/run.ts new file mode 100644 index 000000000..861c75992 --- /dev/null +++ b/src/programs/revenue-analytics/run.ts @@ -0,0 +1,14 @@ +import type { ProgramRun } from '@programs/program-run'; +import { REVENUE_ABORT_CASES } from './abort-cases.js'; + +export const REVENUE_ANALYTICS_RUN: ProgramRun = { + skillId: 'revenue-analytics-setup', + integrationLabel: 'revenue-analytics-setup', + customPrompt: () => 'Set up revenue analytics for this project.', + successMessage: 'Revenue analytics configured!', + reportFile: 'posthog-revenue-report.md', + docsUrl: 'https://posthog.com/docs/revenue-analytics', + spinnerMessage: 'Setting up revenue analytics...', + estimatedDurationMinutes: 5, + abortCases: REVENUE_ABORT_CASES, +}; diff --git a/src/programs/web-analytics-doctor/abort-cases.ts b/src/programs/web-analytics-doctor/abort-cases.ts new file mode 100644 index 000000000..7b5fbd129 --- /dev/null +++ b/src/programs/web-analytics-doctor/abort-cases.ts @@ -0,0 +1,34 @@ +import type { AbortCase } from '@agent/types'; +import { ErrorCodes } from '@shared/errors'; + +export const WEB_ANALYTICS_ABORT_CASES: AbortCase[] = [ + { + match: /^no web analytics events$/i, + message: 'No web analytics events', + body: + 'The doctor found no $pageview events in the last 30 days, so there is ' + + 'nothing to audit yet. Make sure PostHog is initialized and capturing ' + + 'pageviews, then run the doctor again.', + docsUrl: 'https://posthog.com/docs/web-analytics/getting-started', + }, + { + match: /^insufficient permissions$/i, + errorCode: ErrorCodes.AuthMissingScope, + message: 'Insufficient permissions', + body: + 'The doctor could not query your project — the authenticated token is ' + + 'missing query access. Re-run the wizard to sign in again, or use a key ' + + 'with read access to your events.', + docsUrl: 'https://posthog.com/docs/web-analytics', + }, + { + match: /^posthog sdk not installed$/i, + errorCode: ErrorCodes.DetectNoPosthogSdk, + message: 'PostHog SDK not installed', + body: + 'The doctor could not find a PostHog SDK in this project. Install and ' + + 'configure PostHog first (run `npx @posthog/wizard`), then run the ' + + 'doctor to check your web analytics setup.', + docsUrl: 'https://posthog.com/docs/libraries/js', + }, +]; diff --git a/src/programs/web-analytics-doctor/detect.ts b/src/programs/web-analytics-doctor/detect.ts index e7df63673..6340ff6e9 100644 --- a/src/programs/web-analytics-doctor/detect.ts +++ b/src/programs/web-analytics-doctor/detect.ts @@ -1,7 +1,5 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; -import type { AbortCase } from '@agent/types'; -import { ErrorCodes } from '@shared/errors'; import { findPackageJsons } from '@programs/shared/package-scanning'; export type WebAnalyticsDetectError = @@ -13,37 +11,7 @@ export type WebAnalyticsDetectError = | { kind: 'no-package-json' } | { kind: 'no-posthog'; scannedCount: number }; -export const WEB_ANALYTICS_ABORT_CASES: AbortCase[] = [ - { - match: /^no web analytics events$/i, - message: 'No web analytics events', - body: - 'The doctor found no $pageview events in the last 30 days, so there is ' + - 'nothing to audit yet. Make sure PostHog is initialized and capturing ' + - 'pageviews, then run the doctor again.', - docsUrl: 'https://posthog.com/docs/web-analytics/getting-started', - }, - { - match: /^insufficient permissions$/i, - errorCode: ErrorCodes.AuthMissingScope, - message: 'Insufficient permissions', - body: - 'The doctor could not query your project — the authenticated token is ' + - 'missing query access. Re-run the wizard to sign in again, or use a key ' + - 'with read access to your events.', - docsUrl: 'https://posthog.com/docs/web-analytics', - }, - { - match: /^posthog sdk not installed$/i, - errorCode: ErrorCodes.DetectNoPosthogSdk, - message: 'PostHog SDK not installed', - body: - 'The doctor could not find a PostHog SDK in this project. Install and ' + - 'configure PostHog first (run `npx @posthog/wizard`), then run the ' + - 'doctor to check your web analytics setup.', - docsUrl: 'https://posthog.com/docs/libraries/js', - }, -]; +export { WEB_ANALYTICS_ABORT_CASES } from './abort-cases.js'; export function detectWebAnalyticsPrerequisites( session: WizardSession, diff --git a/src/programs/web-analytics-doctor/index.ts b/src/programs/web-analytics-doctor/index.ts index 4d11413c2..a7c2863ce 100644 --- a/src/programs/web-analytics-doctor/index.ts +++ b/src/programs/web-analytics-doctor/index.ts @@ -1,33 +1,10 @@ import type { ProgramConfig } from '@programs/program-step'; import { createSkillProgram } from '../agent-skill/index.js'; import { WEB_ANALYTICS_DOCTOR_PROGRAM } from './steps.js'; -import { WEB_ANALYTICS_ABORT_CASES } from './detect.js'; - -const REPORT_FILE = 'posthog-web-analytics-report.md'; -const DOCS_URL = 'https://posthog.com/docs/web-analytics'; +import { WEB_ANALYTICS_DOCTOR_OPTIONS } from './run.js'; export const webAnalyticsDoctorConfig: ProgramConfig = { - ...createSkillProgram({ - skillId: 'web-analytics-doctor', - command: 'web-analytics', - id: 'web-analytics-doctor', - description: 'Audit and fix your PostHog web analytics setup', - integrationLabel: 'web-analytics-doctor', - customPrompt: - "Run the web-analytics-doctor skill to check this project's PostHog web " + - 'analytics setup. Audit read-only first, then present the findings to the ' + - 'user with a single wizard_ask multi-select and apply only the fixes they ' + - 'choose — editing project code and/or PostHog project settings via the ' + - 'MCP — before writing the report.', - successMessage: - 'Web analytics check complete! You can view the report at ./posthog-web-analytics-report.md', - reportFile: REPORT_FILE, - docsUrl: DOCS_URL, - spinnerMessage: 'Checking your web analytics setup...', - estimatedDurationMinutes: 5, - requires: ['posthog-integration'], - abortCases: WEB_ANALYTICS_ABORT_CASES, - }), + ...createSkillProgram(WEB_ANALYTICS_DOCTOR_OPTIONS), steps: WEB_ANALYTICS_DOCTOR_PROGRAM, parentCommand: 'audit', }; diff --git a/src/programs/web-analytics-doctor/run.ts b/src/programs/web-analytics-doctor/run.ts new file mode 100644 index 000000000..cd6bf6b44 --- /dev/null +++ b/src/programs/web-analytics-doctor/run.ts @@ -0,0 +1,27 @@ +import type { SkillProgramOptions } from '@programs/agent-skill/run-definition'; +import { WEB_ANALYTICS_ABORT_CASES } from './abort-cases.js'; + +const REPORT_FILE = 'posthog-web-analytics-report.md'; +const DOCS_URL = 'https://posthog.com/docs/web-analytics'; + +export const WEB_ANALYTICS_DOCTOR_OPTIONS: SkillProgramOptions = { + skillId: 'web-analytics-doctor', + command: 'web-analytics', + id: 'web-analytics-doctor', + description: 'Audit and fix your PostHog web analytics setup', + integrationLabel: 'web-analytics-doctor', + customPrompt: + "Run the web-analytics-doctor skill to check this project's PostHog web " + + 'analytics setup. Audit read-only first, then present the findings to the ' + + 'user with a single wizard_ask multi-select and apply only the fixes they ' + + 'choose — editing project code and/or PostHog project settings via the ' + + 'MCP — before writing the report.', + successMessage: + 'Web analytics check complete! You can view the report at ./posthog-web-analytics-report.md', + reportFile: REPORT_FILE, + docsUrl: DOCS_URL, + spinnerMessage: 'Checking your web analytics setup...', + estimatedDurationMinutes: 5, + requires: ['posthog-integration'], + abortCases: WEB_ANALYTICS_ABORT_CASES, +}; From 793a34fd41927ddd7098c44a6eb03e4ac065a3dd Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 15:40:16 -0400 Subject: [PATCH 54/61] refactor(programs): own binding policy and experiment selection Move program routing out of the agent: - PROGRAM_BINDINGS and resolveProgramBinding go to src/programs/binding.ts. - The flag experiments move from src/agent/runner/switchboard/flags to src/programs/experiments, with their tests. - The program-axis commandments go to src/programs/commandments.ts, and the switchboard telemetry to src/programs/binding-telemetry.ts. - The switchboard and variant-gating tests move to src/programs. - DEFAULT_AGENT_BINDING, in src/agent/default-binding.ts, is the route standalone callers pass. RunConfig drops switchboard. The caller passes what it already resolved: programCommandments, stageOverrides, seededTasksEnabled and binding.roleBindings. The orchestrator reads the role routes and the stage and seeded-task policy from them. Pi assembles the supplied commandments on both sequences, and Anthropic on task runs. Anthropic's linear run looked guidance up by the run label, which names no program, so it still gets none. The agent's resolveBinding and resolveHarness take the base binding and the flag route as data. Behavior is unchanged. For every program, across 22 flag and CLI cases, in dev, production and cloud-surface builds, the resolved binding, trace, role routes, stage overrides, seeded-task gate and commandments match the previous code. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .claude/skills/adding-skill-program/SKILL.md | 4 +- .claude/skills/wizard-development/SKILL.md | 6 +- .../references/ARCHITECTURE.md | 18 +-- AGENTS.md | 2 +- docs/benchmarking.md | 2 +- scripts/a3-fault-probe.no-jest.ts | 5 - .../architecture/known-violations.json | 3 - src/agent/README.md | 20 ++-- src/agent/__tests__/commandments.test.ts | 10 +- .../__tests__/run-agent-standalone.test.ts | 42 +------ src/agent/agent-interface.ts | 10 +- src/agent/default-binding.ts | 10 ++ src/agent/index.ts | 17 +-- src/agent/runner/README.md | 2 +- .../__tests__/pending-question.test.ts | 1 - src/agent/runner/harness/anthropic/index.ts | 1 + src/agent/runner/harness/pi/index.ts | 2 +- src/agent/runner/harness/pi/task.ts | 2 +- src/agent/runner/sequence/README.md | 5 +- .../orchestrator/orchestrator-runner.ts | 30 ++--- src/agent/runner/shared/types.ts | 19 +-- src/agent/runner/switchboard/commandments.ts | 29 +---- src/agent/runner/switchboard/harness.ts | 32 +++-- src/agent/runner/switchboard/index.ts | 98 +++------------- src/agent/runner/switchboard/sequence.ts | 25 ++-- src/agent/types.ts | 6 +- .../__tests__/switchboard.test.ts | 12 +- .../__tests__/variant-gating.test.ts | 2 +- src/programs/binding-telemetry.ts | 50 ++++++++ src/programs/binding.ts | 109 ++++++++++++++++++ src/programs/commandments.ts | 18 +++ .../experiments}/__tests__/binding-cases.ts | 9 +- .../experiments}/__tests__/flags.test.ts | 34 +++--- .../flags => programs/experiments}/index.ts | 0 .../experiments}/orchestrator.ts | 0 .../flags => programs/experiments}/schemes.ts | 2 +- .../experiments}/self-driving.ts | 0 src/programs/run-agent-legacy.ts | 73 +++--------- src/programs/types.ts | 1 + 39 files changed, 376 insertions(+), 335 deletions(-) create mode 100644 src/agent/default-binding.ts rename src/{agent/runner => programs}/__tests__/switchboard.test.ts (97%) rename src/{agent => programs}/__tests__/variant-gating.test.ts (89%) create mode 100644 src/programs/binding-telemetry.ts create mode 100644 src/programs/binding.ts create mode 100644 src/programs/commandments.ts rename src/{agent/runner/switchboard/flags => programs/experiments}/__tests__/binding-cases.ts (91%) rename src/{agent/runner/switchboard/flags => programs/experiments}/__tests__/flags.test.ts (93%) rename src/{agent/runner/switchboard/flags => programs/experiments}/index.ts (100%) rename src/{agent/runner/switchboard/flags => programs/experiments}/orchestrator.ts (100%) rename src/{agent/runner/switchboard/flags => programs/experiments}/schemes.ts (99%) rename src/{agent/runner/switchboard/flags => programs/experiments}/self-driving.ts (100%) diff --git a/.claude/skills/adding-skill-program/SKILL.md b/.claude/skills/adding-skill-program/SKILL.md index 3aedd8778..28e4ed022 100644 --- a/.claude/skills/adding-skill-program/SKILL.md +++ b/.claude/skills/adding-skill-program/SKILL.md @@ -62,8 +62,8 @@ changing execution behavior. 3. Register the config in [PROGRAM_REGISTRY](../../../src/programs/program-registry.ts) and add its Pi/orchestrator entry to - [PROGRAM_BINDINGS](../../../src/agent/runner/switchboard/index.ts). - [Existing binding checks](../../../src/agent/runner/__tests__/switchboard.test.ts) + [PROGRAM_BINDINGS](../../../src/programs/binding.ts). + [Existing binding checks](../../../src/programs/__tests__/switchboard.test.ts) enforce coverage; `ProgramId` currently widens to `string`. 4. For a standalone native command, create a command module with [nativeCommandFactory](../../../src/commands/factories/native-command-factory.ts) diff --git a/.claude/skills/wizard-development/SKILL.md b/.claude/skills/wizard-development/SKILL.md index 5fee5c7a5..c9819acd4 100644 --- a/.claude/skills/wizard-development/SKILL.md +++ b/.claude/skills/wizard-development/SKILL.md @@ -22,7 +22,7 @@ infrastructure should consume those boundaries. | Framework detection, context, env conventions | [FrameworkConfig](../../../src/programs/framework-config.ts) and [framework configs](../../../src/programs/frameworks/) | | Integration instructions and orchestrator flows/tasks | [context-mill](https://github.com/PostHog/context-mill) | | Programs, steps, prerequisites and outcomes | [programs](../../../src/programs/) | -| Sequence, harness, model and effort selection | [switchboard](../../../src/agent/runner/switchboard/) | +| Sequence, harness, model and effort selection | [program bindings](../../../src/programs/binding.ts) and [agent clamps](../../../src/agent/runner/switchboard/) | | Local tool permissions and scanner adapters | [agent-interface](../../../src/agent/agent-interface.ts), [YARA hooks](../../../src/agent/yara-hooks.ts), [Pi security](../../../src/agent/runner/harness/pi/security.ts) | | Scanner rules | [warlock](https://github.com/PostHog/warlock) | | Token admission and budgets | [PostHog mint endpoint](https://github.com/PostHog/posthog/blob/master/posthog/llm/wizard_gateway_token.py) and [ai-gateway](https://github.com/PostHog/ai-gateway) | @@ -43,8 +43,8 @@ infrastructure should consume those boundaries. new Anthropic models. Existing routing has not all migrated: -[DEFAULT_BINDING](../../../src/agent/runner/switchboard/index.ts) still -selects Anthropic + linear, with per-program and flag overrides. Set new +[DEFAULT_AGENT_BINDING](../../../src/agent/default-binding.ts) selects Pi + linear +for standalone runs; programs apply their own binding and flag overrides. Set new bindings explicitly. Migrating an existing program requires checking its flow, tasks, and lifecycle hooks; changing the default constant alone is insufficient. Both harnesses implement `run` and `runTask`. diff --git a/.claude/skills/wizard-development/references/ARCHITECTURE.md b/.claude/skills/wizard-development/references/ARCHITECTURE.md index 570d1e5e1..72a0228b4 100644 --- a/.claude/skills/wizard-development/references/ARCHITECTURE.md +++ b/.claude/skills/wizard-development/references/ARCHITECTURE.md @@ -60,10 +60,11 @@ example. Native command modules still need registration in ## Switchboard contract -`resolveBinding(ctx, role?)` is the routing seam. Read its exported types rather -than copying their fields into another document. It receives the program, flag -snapshot/payloads, composition state, and development overrides, returning the -binding and stamping a trace of the selected precedence rungs. +`resolveProgramBinding(ctx)` in [programs](../../../../src/programs/binding.ts) +is the routing seam. It receives the program, flag snapshot/payloads, +composition state and development overrides, returning a resolved binding and +stamping a trace of the selected precedence rungs. The agent receives this +binding and any pre-resolved task-role routes as data. - Harness/model: development CLI override, declared flag route, per-program binding, default. @@ -72,7 +73,8 @@ binding and stamping a trace of the selected precedence rungs. [Harness](../../../../src/agent/runner/switchboard/harness.ts) and [sequence](../../../../src/agent/runner/switchboard/sequence.ts) contain the -exact chains. Published builds omit CLI overrides. `RUN_SURFACE` can disable +generic precedence and clamp chains over caller-supplied policy. Published +builds omit CLI overrides. `RUN_SURFACE` can disable harness experiments; static bindings and harness capabilities also affect resolution. Composed sub-runs remain linear even when a CLI override requests orchestration. @@ -84,11 +86,11 @@ admitted by the minted token and gateway. Local routing cannot bypass that external policy; see the [model admission checklist](../SKILL.md#execution-policy-and-model-admission). -Flags belong in -[switchboard/flags](../../../../src/agent/runner/switchboard/flags/). +Program flags belong in +[experiments](../../../../src/programs/experiments/). Experiments declare their program scope; malformed payloads yield no experiment route. Reuse the -[switchboard tests](../../../../src/agent/runner/__tests__/switchboard.test.ts) +[switchboard tests](../../../../src/programs/__tests__/switchboard.test.ts) and experiment tests to check full bindings and isolation of unrelated programs. Do not add a second flag-reading path inside a harness or sequence. diff --git a/AGENTS.md b/AGENTS.md index 773faf9cf..17463d33e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,7 +76,7 @@ Agent SDK is a supported legacy fallback, deprecated as the default; retain it for major Pi vulnerabilities or gaps in support for new Anthropic models. This is the contribution policy, not a claim that every existing binding has -migrated: `DEFAULT_BINDING` is still Anthropic + linear. Set new bindings +migrated. The default, `DEFAULT_AGENT_BINDING`, is Pi + linear. Set new bindings explicitly and check sequence-specific hooks before migrating existing flows. See [execution policy and model admission](.claude/skills/wizard-development/SKILL.md#execution-policy-and-model-admission) diff --git a/docs/benchmarking.md b/docs/benchmarking.md index e5fadf4f7..8d4a15bd9 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -102,7 +102,7 @@ Everything below ships in this repo (`wizard/`) and its workbench orchestrator on pi, per-task models from context-mill frontmatter; off → the linear anthropic default). Per-stage variations ride `wizard-orchestrator-override` payloads (`{stage: {model?, effort?}}`, - variant keys in `wizard/src/agent/runner/switchboard/flags/schemes.ts`). + variant keys in `wizard/src/programs/experiments/schemes.ts`). The baseline is `{"wizard-orchestrator":"false"}` — never an empty override, or live remote flags leak into the baseline. diff --git a/scripts/a3-fault-probe.no-jest.ts b/scripts/a3-fault-probe.no-jest.ts index 91b83e5ac..58ed9d8bd 100644 --- a/scripts/a3-fault-probe.no-jest.ts +++ b/scripts/a3-fault-probe.no-jest.ts @@ -66,11 +66,6 @@ const config: RunConfig = { harness: harness as Harness, model: DEFAULT_AGENT_MODEL, }, - switchboard: { - program: 'fault-probe', - flags: {}, - cliHarness: harness as Harness, - }, skillsBaseUrl: 'http://127.0.0.1:1', wizardFlags: {}, wizardFlagPayloads: {}, diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index c91511369..e1bf710f5 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -1,8 +1,5 @@ { "violations": [ - "src/agent/runner/switchboard/flags/index.ts -> src/programs/types.ts", - "src/agent/runner/switchboard/flags/schemes.ts -> src/programs/types.ts", - "src/agent/runner/switchboard/index.ts -> src/programs/types.ts", "src/commands/ai-observability.ts -> src/programs/ai-observability/index.ts", "src/commands/audit.ts -> src/programs/audit/index.ts", "src/commands/basic-integration/ci-install.ts -> src/programs/posthog-integration/index.ts", diff --git a/src/agent/README.md b/src/agent/README.md index 4116171cd..a3c188057 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -20,10 +20,11 @@ runAgent(config: RunConfig, input: RunInput, options?: { }): Promise ``` -- `RunConfig`: the program id, its `AgentRunDefinition` (prompt, skill, tools, - copy), the resolved `binding` (sequence, harness, model), the switchboard - inputs, the skills origin, flag snapshot, trace tags, tool allow and deny - lists, seed tasks and bound completion `hooks`. +- `RunConfig`: the opaque program id, its `AgentRunDefinition` (prompt, skill, + tools, copy), the resolved `binding` (sequence, harness, model and task-role + routes), supplied program commandments and stage policy, the skills origin, + flag snapshot, trace tags, tool allow and deny lists, seed tasks and bound + completion `hooks`. - `RunInput`: install directory, resolved credentials, project and user payloads, skill id, detected integration, `flags` (`ci`, `signup`, `debug`, `e2eAsk`, `localMcp`, `captureAio`, `benchmark`, `yaraReport`) and the host @@ -54,11 +55,12 @@ runAgent(config: RunConfig, input: RunInput, options?: { `setup wizard finished` event. The host sends it from the outcome: `Success` is `success`, `Aborted` is `cancelled`, `Failed` and `Crashed` are `error`. -Other runtime exports: `resolveBinding`, `shouldDisableAsk`, `initializeAgent`, -`executeAgent`, `buildRunTags`, `AgentSignals`, -`configureGatewayFromCIEnvironment`, `downloadSkill`, `WIZARD_TOOL_NAMES`, -`LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and `runMcpPromptViaSdk`, which -loads the streaming module on first call. +Other runtime exports: `DEFAULT_AGENT_BINDING` for standalone callers, +`resolveHarness` and `resolveBinding`, which programs resolve a binding with, +`shouldDisableAsk`, `initializeAgent`, `executeAgent`, `buildRunTags`, +`AgentSignals`, `configureGatewayFromCIEnvironment`, `downloadSkill`, +`WIZARD_TOOL_NAMES`, `LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and +`runMcpPromptViaSdk`, which loads the streaming module on first call. Minimal invocation: diff --git a/src/agent/__tests__/commandments.test.ts b/src/agent/__tests__/commandments.test.ts index 2a694c0f2..d4a9722d5 100644 --- a/src/agent/__tests__/commandments.test.ts +++ b/src/agent/__tests__/commandments.test.ts @@ -1,5 +1,6 @@ import { WIZARD_COMMANDMENTS } from '@agent/commandments'; import { assembleCommandments } from '@agent/runner/switchboard/commandments'; +import { getProgramCommandments } from '@programs/commandments'; import { Harness, Sequence } from '@shared/constants'; const global = WIZARD_COMMANDMENTS.join('\n'); @@ -10,7 +11,13 @@ const prompt = ( harness: Harness, sequence: Sequence, program = 'posthog-integration', -) => assembleCommandments({ program, sequence, harness, caps: CAPS }); +) => + assembleCommandments({ + programCommandments: getProgramCommandments(program), + sequence, + harness, + caps: CAPS, + }); const COMBOS = [ ['anthropic', Harness.anthropic, Sequence.linear], @@ -148,7 +155,6 @@ describe('commandments by axis', () => { describe('runtime caps gate the pi runtime notes', () => { const withCaps = (caps: { bash: boolean; posthogMcp: boolean }) => assembleCommandments({ - program: 'warehouse-source', sequence: Sequence.linear, harness: Harness.pi, caps, diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index df73af8fe..19e75663a 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -175,6 +175,12 @@ vi.mock('@agent/runner/switchboard/harness', () => { harness: ctx.cliHarness ?? Harness.pi, model: DEFAULT_AGENT_MODEL, }), + resolveRoleHarness: (binding: RunConfig['binding'], role: string) => + binding.roleBindings?.[role] ?? { + harness: binding.harness, + model: binding.model, + thinkingLevel: binding.thinkingLevel, + }, }; }); @@ -237,7 +243,6 @@ const config = (over: Partial = {}): RunConfig => ({ }, composed: false, binding: { sequence: Sequence.linear, harness: Harness.pi, model: 'm' }, - switchboard: { program: 'test-program', flags: {} }, skillsBaseUrl: 'https://skills.test', wizardFlags: {}, wizardFlagPayloads: {}, @@ -316,11 +321,6 @@ describe('runAgent standalone', () => { const running = runAgent( config({ binding: { harness, sequence, model: DEFAULT_AGENT_MODEL }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: harness, - }, }), input(), { interaction: { ask }, onProgress: (event) => events.push(event) }, @@ -387,11 +387,6 @@ describe('runAgent standalone', () => { const result = await runAgent( config({ binding: { harness, sequence, model: DEFAULT_AGENT_MODEL }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: harness, - }, }), input(), ); @@ -427,11 +422,6 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: Harness.anthropic, - }, }), input(), ); @@ -454,11 +444,6 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: Harness.anthropic, - }, }), input(), ); @@ -482,11 +467,6 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: Harness.pi, - }, }), input(), ); @@ -532,11 +512,6 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: Harness.pi, - }, }), input(), { @@ -922,11 +897,6 @@ describe('runAgent standalone', () => { sequence, model: DEFAULT_AGENT_MODEL, }, - switchboard: { - program: 'test-program', - flags: {}, - cliHarness: Harness.pi, - }, }), input(), { diff --git a/src/agent/agent-interface.ts b/src/agent/agent-interface.ts index f19cd85f2..ff13db4d2 100644 --- a/src/agent/agent-interface.ts +++ b/src/agent/agent-interface.ts @@ -211,6 +211,8 @@ export type AgentConfig = { * another program's budget, so neither should depend on an optional string bag. */ programId: string; + /** Program-owned guidance supplied as data, never looked up here. */ + programCommandments?: readonly string[]; /** Program identifier — selects the model for that program. */ integrationLabel?: string; /** @@ -355,8 +357,8 @@ type AgentRunConfig = { * bearer. */ refreshGatewayAuth?: () => Promise; - /** Program id, for the program-axis commandments. */ - program?: string; + /** Program-owned guidance supplied as data. */ + programCommandments?: readonly string[]; /** Resolved sequence, for the sequence-axis commandments. */ sequence: Sequence; /** Where the run reports. A no-op when the caller passed none. */ @@ -646,7 +648,7 @@ export async function initializeAgent( triageProvider, gatewayAuth: auth, refreshGatewayAuth: currentGatewayAuth, - program: config.integrationLabel, + programCommandments: config.programCommandments, // A queue context is present only on a task run; that is the sequence. sequence: config.orchestrator ? Sequence.orchestrator : Sequence.linear, emit, @@ -1146,7 +1148,7 @@ export async function runAgent( // we keep default Claude Code behaviors. An orchestrator context is // present only on a task run — that is what picks the sequence. append: assembleCommandments({ - program: agentConfig.program, + programCommandments: agentConfig.programCommandments, sequence: agentConfig.sequence, harness: Harness.anthropic, }), diff --git a/src/agent/default-binding.ts b/src/agent/default-binding.ts new file mode 100644 index 000000000..7bad35666 --- /dev/null +++ b/src/agent/default-binding.ts @@ -0,0 +1,10 @@ +import { GPT5_6_SOL_MODEL, Harness, Sequence } from '@shared/constants'; +import type { ResolvedBinding } from './runner/shared/types'; + +/** Standalone callers can supply this resolved route without a program registry. */ +export const DEFAULT_AGENT_BINDING: ResolvedBinding = { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_SOL_MODEL, + thinkingLevel: 'medium', +}; diff --git a/src/agent/index.ts b/src/agent/index.ts index 605eeb0c8..577ffa776 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -10,21 +10,22 @@ /** * Stays. The agent's contract: the one way to run it, the marker strings - * program prompts embed, and the tool ids programs put in allowedTools and - * disallowedTools. + * program prompts embed, the tool ids programs put in allowedTools and + * disallowedTools, and what programs resolve a binding with: the default + * binding and the harness axis. */ export type * from './types'; export { runAgent, RunOutcome } from './runner'; export { AgentSignals } from './agent-interface'; export { WIZARD_TOOL_NAMES } from './tools'; +export { DEFAULT_AGENT_BINDING } from './default-binding'; +export { resolveHarness } from './runner/switchboard'; /** - * Leaves in B2 (B1 deferred the bindings). Bindings and program data move to - * programs: resolveBinding - * is keyed by PROGRAM_BINDINGS and the agent keeps only "run from an - * already-resolved binding"; shouldDisableAsk is a flags policy programs - * decide and pass in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own - * as askTimeoutMs. + * Leaves in B2. resolveBinding applies the generic precedence to the base + * binding and routes programs select, and programs/binding.ts is its one + * caller; shouldDisableAsk is a flags policy programs decide and pass in; + * LONGER_ASK_TIMEOUT_MS is a tuning number programs own as askTimeoutMs. */ export { resolveBinding, shouldDisableAsk } from './runner'; export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index 480c1280b..611be4c79 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -23,7 +23,7 @@ retained for very simple tasks and legacy support. The Anthropic Agent SDK is a supported legacy fallback, deprecated as the default, retained for major Pi vulnerabilities or gaps in support for new Anthropic models. -Existing `DEFAULT_BINDING` remains Anthropic + linear; explicit program bindings +`DEFAULT_AGENT_BINDING`, the standalone default, is Pi + linear; explicit program bindings and flags determine actual behavior. Both harnesses implement `run` and `runTask`. Composed sub-runs are clamped to linear, and linear-only post-run/outro hooks do not automatically transfer to an orchestrated flow. diff --git a/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts b/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts index 86ff59c3f..bd7580a5f 100644 --- a/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts +++ b/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts @@ -44,7 +44,6 @@ async function initializeHarness( sequence: Sequence.linear, model: 'test', }, - switchboard: { program: 'test', flags: {} }, skillsBaseUrl: 'https://skills.test', wizardFlags: {}, wizardFlagPayloads: {}, diff --git a/src/agent/runner/harness/anthropic/index.ts b/src/agent/runner/harness/anthropic/index.ts index 8a75169bf..4d622f8ad 100644 --- a/src/agent/runner/harness/anthropic/index.ts +++ b/src/agent/runner/harness/anthropic/index.ts @@ -136,6 +136,7 @@ export const anthropicBackend: AgentHarness = { detectPackageManager: detectNodePackageManagers, skillsBaseUrl: boot.skillsBaseUrl, programId: boot.programId, + programCommandments: config.programCommandments, wizardFlags: boot.wizardFlags, wizardMetadata: boot.wizardMetadata, integrationLabel: config.programId, diff --git a/src/agent/runner/harness/pi/index.ts b/src/agent/runner/harness/pi/index.ts index 90ea3ac54..17e9b6660 100644 --- a/src/agent/runner/harness/pi/index.ts +++ b/src/agent/runner/harness/pi/index.ts @@ -394,7 +394,7 @@ export const piBackend: AgentHarness = { agentDir: getAgentDir(), systemPrompt: assembleCommandments({ - program: runConfig.programId, + programCommandments: runConfig.programCommandments, sequence: Sequence.linear, harness: Harness.pi, caps: { bash: true, posthogMcp }, diff --git a/src/agent/runner/harness/pi/task.ts b/src/agent/runner/harness/pi/task.ts index 290950f40..6caf8e62d 100644 --- a/src/agent/runner/harness/pi/task.ts +++ b/src/agent/runner/harness/pi/task.ts @@ -340,7 +340,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { cwd: input.installDir, agentDir: getAgentDir(), systemPrompt: assembleCommandments({ - program: config.programId, + programCommandments: config.programCommandments, sequence: Sequence.orchestrator, harness: Harness.pi, caps: { bash: codingTools.has('bash'), posthogMcp }, diff --git a/src/agent/runner/sequence/README.md b/src/agent/runner/sequence/README.md index ac4436f9a..ce795d52b 100644 --- a/src/agent/runner/sequence/README.md +++ b/src/agent/runner/sequence/README.md @@ -49,6 +49,7 @@ frontmatter contract rather than copying an old manifest example: task types after planning. It is not a universal automatic graph built from frontmatter. -Per-role `PROGRAM_BINDINGS[id].contextMillOverride` can adjust a task's harness, -model or effort. New task types usually belong in context-mill; new native +Per-role routes in the resolved binding can adjust a task's harness, model or +effort. Programs resolve `PROGRAM_BINDINGS[id].contextMillOverride` before the +agent starts. New task types usually belong in context-mill; new native behavior still requires the appropriate Wizard configuration or implementation. diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index a5d42f8ab..850453cb3 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -43,10 +43,8 @@ import type { import { createEmitSpinner } from '../../shared/progress-collector'; import { createAskBridge } from '../../shared/ask'; import { - areSeededTasksEnabled, getHarness, - resolveHarness, - resolveStageOverrides, + resolveRoleHarness, type HarnessPick, } from '../../switchboard'; import { isValidModel, requireKnownModel } from '../../switchboard/models'; @@ -608,11 +606,6 @@ async function executeOrchestrator( const { run } = config; const programId = config.programId; - // Switchboard context — reused for every per-role harness resolution below. - // The caller resolved the run-level binding from it; per-task roles overlay - // `binding.contextMillOverride[role]` on the same inputs. - const switchboardCtx = { ...config.switchboard, trace: undefined }; - // The WHAT (agent prompts) is served from context-mill. Fetch the registry // once up front: its types drive enqueue validation, and resolving a task to // its run config is then synchronous, with no mid-drain network latency. @@ -620,11 +613,7 @@ async function executeOrchestrator( const registry = await loadAgentRegistry(boot.skillsBaseUrl, flow, { exclude: effectiveExcludedTaskTypes(config, boot.wizardFlags), // Baked into the prompts at load, so enqueue, dispatch, and telemetry all read one effective spec. - overrides: resolveStageOverrides( - programId, - boot.wizardFlags, - boot.wizardFlagPayloads, - ), + overrides: config.stageOverrides, }); if (signal?.aborted) return cancelledRun(); const seedPrompt = registry.seed; @@ -638,7 +627,7 @@ async function executeOrchestrator( const taskModels = Object.fromEntries( ['seed', ...registry.types].map((type) => { const prompt = type === 'seed' ? seedPrompt : registry.get(type); - const pick = resolveHarness(switchboardCtx, type); + const pick = resolveRoleHarness(config.binding, type); const specModel = prompt && promptModelFor(prompt, pick.harness).model; return [type, isValidModel(specModel) ? specModel : pick.model]; }), @@ -663,7 +652,7 @@ async function executeOrchestrator( const store = new QueueStore(input.installDir, runId, { onTransition: (event, task) => { - const pick = resolveHarness(switchboardCtx, task.type); + const pick = resolveRoleHarness(config.binding, task.type); // Mirror dispatch's allow-list fallback so attribution names the model that runs. const specModel = taskModelSpec(registry, task, pick.harness).model; const base = { @@ -880,7 +869,7 @@ async function executeOrchestrator( // depend on them, and no prompt has to remember they are there. // Kill switch: off (or unset), the wizard queues nothing itself and the run // is byte-identical to a project with no detected sources. - const seedEntries = areSeededTasksEnabled(boot.wizardFlags) + const seedEntries = config.seededTasksEnabled ? config.seedTasks?.() ?? [] : []; const seededTypes: string[] = []; @@ -997,7 +986,7 @@ async function executeOrchestrator( // Prompt-frontmatter model wins over the switchboard pick (§3.6 of the // switchboard plan) — the switchboard's model is the fallback when the // prompt is silent. - const seedPick = resolveHarness(switchboardCtx, 'seed'); + const seedPick = resolveRoleHarness(config.binding, 'seed'); const seedHarness = requireTaskHarness(seedPick); const seedModel = promptModelFor(seedPrompt, seedPick.harness); const seedResult = await seedHarness.runTask({ @@ -1207,10 +1196,9 @@ async function executeOrchestrator( // panel shows progress); errors still surface — the harness stops the // spinner with its own error text. // - // Per-task role = task.type — the switchboard consults - // PROGRAM_BINDINGS[id].contextMillOverride?.[task.type] for wizard-side - // per-agent overrides. Prompt-frontmatter model still wins (§3.6). - const taskPick = resolveHarness(switchboardCtx, task.type); + // Per-task role = task.type. Programs resolved any role override before + // invocation; prompt-frontmatter model still wins (§3.6). + const taskPick = resolveRoleHarness(config.binding, task.type); const taskHarness = requireTaskHarness(taskPick); const taskModel = taskModelSpec(registry, task, taskPick.harness); let taskResult: AgentResult; diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index b82f7af7b..9f2efd792 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -21,7 +21,6 @@ import type { ErrorCode } from '@shared/errors'; import type { LLMProvider } from '@posthog/warlock'; import type { AgentInteraction, ProgressEmitter } from '@agent/progress'; import type { EffortLevel } from '../switchboard/models'; -import type { SwitchboardCtx } from '../switchboard'; export type { PromptContext, Credentials }; @@ -139,6 +138,11 @@ export interface ResolvedBinding { model: string; /** Reasoning-effort override. Absent → the model's table default. */ thinkingLevel?: EffortLevel; + /** Role-specific routes resolved by the caller before the agent starts. */ + roleBindings?: Record< + string, + { harness: Harness; model: string; thinkingLevel?: EffortLevel } + >; } /** @@ -147,7 +151,7 @@ export interface ResolvedBinding { * treats every label as opaque. */ export interface RunConfig { - /** Program id: gateway spend pin, analytics label, commandments axis. */ + /** Opaque program label for gateway spend pin and analytics. */ programId: string; /** The run definition. A program's session-taking hooks are the caller's, see `hooks`. */ run: AgentRunDefinition; @@ -155,11 +159,12 @@ export interface RunConfig { composed: boolean; /** Run-level sequence, harness and model. */ binding: ResolvedBinding; - /** - * The inputs the run-level binding was resolved from. The orchestrator - * re-resolves the harness per task role from these; nothing else reads them. - */ - switchboard: SwitchboardCtx; + /** Program text selected by the caller; the agent only assembles it. */ + programCommandments?: readonly string[]; + /** Validated stage policy selected by the caller; absent keeps flow frontmatter. */ + stageOverrides?: Record; + /** The caller's resolved seeded-task experiment. */ + seededTasksEnabled?: boolean; /** Primary skills origin (context-mill dev or GitHub Releases). */ skillsBaseUrl: string; /** Feature flag key → variant, evaluated before the run. */ diff --git a/src/agent/runner/switchboard/commandments.ts b/src/agent/runner/switchboard/commandments.ts index 5f709b1fb..a170b271a 100644 --- a/src/agent/runner/switchboard/commandments.ts +++ b/src/agent/runner/switchboard/commandments.ts @@ -1,11 +1,8 @@ /** * System-prompt commandments, keyed by the axes the switchboard resolves. * - * A run is a resolved (program, sequence, harness, model). Guidance belongs to - * whichever axis makes it true, declared here beside the tables that resolve - * them, and assembled once by `assembleCommandments`. A rule that is true for - * every run stays in `@lib/agent/commandments`; anything narrower lives here so - * the call sites never re-derive it. + * Programs select their guidance before the run; the agent assembles that + * supplied text with global, sequence, model and harness guidance. * * Leaf module by design — it imports the axis enums and the per-axis text, never * a runner or a harness backend, so the harnesses can call the assembler without @@ -40,20 +37,6 @@ const SEQUENCE_COMMANDMENTS: Record = { [Sequence.orchestrator]: [], }; -// ── Program axis ──────────────────────────────────────────────────────── - -const SELF_DRIVING = [ - 'ALWAYS surface a custom-scout proposal in step 6b: bring the user your one or two strongest candidate scouts even when the built-in troop looks sufficient. The proposal ask leads with a "None — keep the built-in troop" option, so declining costs the user one keystroke — but a proposal you silently skip is coverage they never got to see or judge. Where the skill says to skip the ask when the gap analysis finds no candidate, do NOT skip: pick your best candidates anyway and let the user decide.', - - 'Rank candidates at the discriminator level, not the category level. "Covered" only means an enabled scout would actually FIRE for that failure mode: a conversion-rate watcher does not catch entry volume collapsing; a Stripe-transaction watcher does not catch a lead form going silent. A surface whose failure mode has no firing condition among the enabled scouts is your strongest candidate.', - - 'Be honest in the option descriptions: if a candidate overlaps something an enabled scout partially watches, say so in its description rather than dropping the candidate. The user chooses with full information; you do not gatekeep on their behalf.', -]; - -const PROGRAM_COMMANDMENTS: Record = { - 'self-driving': SELF_DRIVING, -}; - // ── Harness axis ──────────────────────────────────────────────────────── /** @@ -75,8 +58,8 @@ const MODEL_COMMANDMENTS: Record = {}; // ── Assembly ──────────────────────────────────────────────────────────── export interface CommandmentAxes { - /** Program id, as resolved into `PROGRAM_BINDINGS`. */ - program?: string; + /** Selected by programs; the agent only assembles supplied text. */ + programCommandments?: readonly string[]; sequence: Sequence; harness: Harness; /** Gateway model id. */ @@ -87,14 +70,14 @@ export interface CommandmentAxes { /** Every commandment this run's axes call for, broad to narrow. */ export function assembleCommandments(axes: CommandmentAxes): string { - const { program, sequence, harness, model, caps } = axes; + const { programCommandments, sequence, harness, model, caps } = axes; const harnessNotes = HARNESS_NOTES[harness]?.( sequence, caps ?? { bash: true, posthogMcp: true }, ); return [ ...WIZARD_COMMANDMENTS, - ...(program ? PROGRAM_COMMANDMENTS[program] ?? [] : []), + ...(programCommandments ?? []), ...SEQUENCE_COMMANDMENTS[sequence], ...(model ? MODEL_COMMANDMENTS[model] ?? [] : []), // Blank line first: the notes open their own `## This runtime` section. diff --git a/src/agent/runner/switchboard/harness.ts b/src/agent/runner/switchboard/harness.ts index 0f5673ed2..edfac1b59 100644 --- a/src/agent/runner/switchboard/harness.ts +++ b/src/agent/runner/switchboard/harness.ts @@ -8,10 +8,8 @@ import { logToFile } from '@utils/debug'; import { anthropicBackend } from '../harness/anthropic'; import { piBackend } from '../harness/pi'; import type { AgentHarness } from '../harness/types'; -import { resolveFlagRoute } from './flags'; import { DEFAULT_BINDING, - PROGRAM_BINDINGS, runChain, type HarnessPick, type Middleware, @@ -32,12 +30,11 @@ export function getHarness(name: Harness): AgentHarness { } /** - * PostHog-flag routing to pi (see `./flags`). No valid route — flag off, no - * config, or an invalid payload — keeps the non-flagged binding default. + * A validated caller-supplied route overlays the base binding. */ const flagRunnerOverride: Middleware = (ctx, next) => { const pick = next(); - const route = resolveFlagRoute(ctx.program, ctx.flags, ctx.flagPayloads); + const route = ctx.flagRoute; if (!route) return pick; if (ctx.trace) { ctx.trace.harness = 'flag'; @@ -86,7 +83,7 @@ export function resolveHarness( const pick = runChain(HARNESS_MIDDLEWARE, ctx, () => { if (ctx.trace) Object.assign(ctx.trace, { harness: 'binding', model: 'binding' }); - const binding = PROGRAM_BINDINGS[ctx.program] ?? DEFAULT_BINDING; + const binding = ctx.baseBinding ?? DEFAULT_BINDING; return { harness: binding.harness, model: binding.model, @@ -95,7 +92,9 @@ export function resolveHarness( }; }); logToFile( - `[switchboard] resolved: program=${ctx.program} harness=${pick.harness}` + + `[switchboard] resolved: program=${ctx.program ?? '?'} harness=${ + pick.harness + }` + `${ctx.trace?.harness ? ` (${ctx.trace.harness})` : ''} model=${ pick.model }` + @@ -103,3 +102,22 @@ export function resolveHarness( ); return pick; } + +/** The agent resolves a task role only from data the caller already supplied. */ +export function resolveRoleHarness( + binding: { + harness: Harness; + model: string; + thinkingLevel?: HarnessPick['thinkingLevel']; + roleBindings?: Record; + }, + role: string, +): HarnessPick { + return ( + binding.roleBindings?.[role] ?? { + harness: binding.harness, + model: binding.model, + thinkingLevel: binding.thinkingLevel, + } + ); +} diff --git a/src/agent/runner/switchboard/index.ts b/src/agent/runner/switchboard/index.ts index b2d84add2..a1a668ee0 100644 --- a/src/agent/runner/switchboard/index.ts +++ b/src/agent/runner/switchboard/index.ts @@ -1,13 +1,7 @@ // Resolves routing; model additions also require mint allowlists and gateway prompt/transport support. -import { - DEFAULT_AGENT_MODEL, - GPT5_6_SOL_MODEL, - GPT5_6_TERRA_MODEL, - Harness, - Sequence, -} from '@shared/constants'; -import type { ProgramId } from '@programs/types'; +import { Harness, Sequence } from '@shared/constants'; +import { DEFAULT_AGENT_BINDING } from '@agent/default-binding'; import { resolveHarness } from './harness'; import type { EffortLevel } from './models'; import { resolveSequence } from './sequence'; @@ -29,12 +23,22 @@ export interface SwitchboardTrace { /** Everything a resolver middleware may branch on. Built once per run. */ export interface SwitchboardCtx { - program: ProgramId; + /** Opaque log label. Program lookup stays with the caller. */ + program?: string; + /** The caller's selected base binding, before route/CLI overlays. */ + baseBinding?: ProgramBinding; /** Composed sub-run (a dependency inside a parent program). Structurally linear — no override can orchestrate it. */ composed?: boolean; - flags: Record; - /** Flag payloads from the same snapshot (payload-carrying flags, e.g. self-driving pi). */ - flagPayloads?: Record; + /** Already validated experiment route; no flag parsing happens in the agent. */ + flagRoute?: { + harness?: Harness; + model?: string; + thinkingLevel?: EffortLevel; + sequence?: Sequence; + }; + flagSequence?: Sequence; + /** Raw boolean only for the existing capability-clamp log line. */ + orchestratorFlagOn?: boolean; /** CLI override (`--harness`). Wins over `flags`. */ cliHarness?: Harness; /** CLI override (`--sequence`). Wins over `flags`. */ @@ -102,68 +106,8 @@ export interface ProgramBinding { contextMillOverride?: Record>; } -// Legacy fallback; new programs should explicitly choose Pi and prefer orchestration. -export const DEFAULT_BINDING: ProgramBinding = { - sequence: Sequence.linear, - harness: Harness.pi, - model: GPT5_6_SOL_MODEL, - thinkingLevel: 'medium', -}; - -/** - * Per-program routing. Kept in lockstep with `PROGRAM_REGISTRY` by the - * switchboard test. Anything absent falls back to `DEFAULT_BINDING`. - */ -export const PROGRAM_BINDINGS: Partial> = { - 'posthog-integration': DEFAULT_BINDING, - 'revenue-analytics-setup': DEFAULT_BINDING, - 'warehouse-source': DEFAULT_BINDING, - 'error-tracking-upload-source-maps': { - sequence: Sequence.linear, - harness: Harness.pi, - model: GPT5_6_SOL_MODEL, - thinkingLevel: 'medium', - }, - audit: DEFAULT_BINDING, - 'events-audit': DEFAULT_BINDING, - 'posthog-doctor': DEFAULT_BINDING, - 'web-analytics-doctor': DEFAULT_BINDING, - migration: DEFAULT_BINDING, - 'self-driving': DEFAULT_BINDING, - 'agent-skill': DEFAULT_BINDING, - 'mcp-add': DEFAULT_BINDING, - 'mcp-remove': DEFAULT_BINDING, - 'mcp-tutorial': DEFAULT_BINDING, - 'mcp-analytics': DEFAULT_BINDING, - // Orchestrator on pi. The binding routes only; every stage's model and - // effort are pinned context-mill side in the flow's frontmatter - // (`model_pi`/`effort_pi`: terra seed, sol tasks, luna report). - metrics: { - sequence: Sequence.orchestrator, - harness: Harness.pi, - model: DEFAULT_AGENT_MODEL, - }, - 'replay-vision': { - sequence: Sequence.orchestrator, - harness: Harness.anthropic, - model: DEFAULT_AGENT_MODEL, - }, - // Orchestrator on pi, like metrics. The binding routes only; every stage's - // model and effort are pinned context-mill side in the flow's frontmatter - // (`model_pi`/`effort_pi`: terra seed, install and init, sol tasks, luna report). - 'error-tracking': { - sequence: Sequence.orchestrator, - harness: Harness.pi, - model: DEFAULT_AGENT_MODEL, - }, - 'ai-observability': { - sequence: Sequence.linear, - harness: Harness.pi, - model: GPT5_6_TERRA_MODEL, - thinkingLevel: 'high', - }, - slack: DEFAULT_BINDING, -}; +/** Legacy alias until the public runner export is removed in B2 integration. */ +export const DEFAULT_BINDING: ProgramBinding = DEFAULT_AGENT_BINDING; // ── Unified resolver ──────────────────────────────────────────────────── @@ -186,8 +130,4 @@ export { resolveSequence, type SequenceRunner, } from './sequence'; -export { - isOrchestratorEnabled, - areSeededTasksEnabled, - resolveStageOverrides, -} from './flags'; +export { resolveRoleHarness } from './harness'; diff --git a/src/agent/runner/switchboard/sequence.ts b/src/agent/runner/switchboard/sequence.ts index b6d241720..09af8843a 100644 --- a/src/agent/runner/switchboard/sequence.ts +++ b/src/agent/runner/switchboard/sequence.ts @@ -1,23 +1,16 @@ /** - * Sequence axis: gate helpers, registry, middleware, resolver. - * Percentage rollouts are PostHog-side — the gate just reads the resolved bool. + * Sequence axis: registry, clamps and generic resolution of caller-supplied policy. */ import { IS_PRODUCTION_BUILD } from '@env'; import { Sequence } from '@shared/constants'; import { logToFile } from '@utils/debug'; -import { - isOrchestratorEnabled, - resolveFlagRoute, - resolveFlagSequence, -} from './flags'; import { getHarness, resolveHarness } from './harness'; import type { SequenceResult, SequenceContext } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; import { DEFAULT_BINDING, - PROGRAM_BINDINGS, runChain, type Middleware, type SwitchboardCtx, @@ -70,17 +63,17 @@ const cliSequenceMw: Middleware = (ctx, next) => { return ctx.cliSequence; }; -/** A program's own flag route may pin the sequence; wins over the global orchestrator flag. Traced as 'payload' to stay distinguishable from sequence experiments ('flag'). */ +/** A caller-supplied payload route may pin the sequence. */ const flagRouteSequenceMw: Middleware = (ctx, next) => { - const route = resolveFlagRoute(ctx.program, ctx.flags, ctx.flagPayloads); + const route = ctx.flagRoute; if (!route?.sequence) return next(); if (ctx.trace) ctx.trace.sequence = 'payload'; return route.sequence; }; -/** Sequence experiments (e.g. wizard-orchestrator), each inert outside its declared programs. */ +/** A caller-supplied sequence experiment, already scoped to its program. */ const sequenceExperimentMw: Middleware = (ctx, next) => { - const sequence = resolveFlagSequence(ctx.program, ctx.flags); + const sequence = ctx.flagSequence; if (!sequence) return next(); if (ctx.trace) ctx.trace.sequence = 'flag'; return sequence; @@ -96,7 +89,7 @@ const sequenceExperimentMw: Middleware = (ctx, next) => { const runTaskCapabilityClampMw: Middleware = (ctx, next) => { const pick = resolveHarness(ctx); if (getHarness(pick.harness).runTask) return next(); - if (isOrchestratorEnabled(ctx.flags)) { + if (ctx.orchestratorFlagOn) { logToFile( `[switchboard] wizard-orchestrator ignored: ${pick.harness} has no runTask, clamping to linear`, ); @@ -119,11 +112,13 @@ const SEQUENCE_MIDDLEWARE: Middleware[] = [ export function resolveSequence(ctx: SwitchboardCtx): Sequence { const sequence = runChain(SEQUENCE_MIDDLEWARE, ctx, () => { if (ctx.trace) ctx.trace.sequence = 'binding'; - const binding = PROGRAM_BINDINGS[ctx.program] ?? DEFAULT_BINDING; + const binding = ctx.baseBinding ?? DEFAULT_BINDING; return binding.sequence; }); logToFile( - `[switchboard] resolved: program=${ctx.program} sequence=${sequence}` + + `[switchboard] resolved: program=${ + ctx.program ?? '?' + } sequence=${sequence}` + `${ctx.trace?.sequence ? ` (${ctx.trace.sequence})` : ''}`, ); return sequence; diff --git a/src/agent/types.ts b/src/agent/types.ts index e3476645b..2b4bd4352 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -12,8 +12,11 @@ export type { AgentRunDefinition, PromptContext, RunConfig, + ResolvedBinding, + RunFlags, RunInput, RunResult, + SeedTaskEntry, } from './runner'; export type { AgentInteraction, @@ -28,8 +31,9 @@ export type { TokenUsageDelta, } from './progress'; -/** Leaves in B2 with the bindings table; B1 deferred PROGRAM_BINDINGS. */ +/** Input types of the exported resolveBinding and resolveHarness. */ export type { ProgramBinding, SwitchboardCtx } from './runner'; +export type { EffortLevel } from './runner/switchboard/models'; /** Leaves in B2 with downloadSkill. */ export type { InstallSkillResult } from './tools'; diff --git a/src/agent/runner/__tests__/switchboard.test.ts b/src/programs/__tests__/switchboard.test.ts similarity index 97% rename from src/agent/runner/__tests__/switchboard.test.ts rename to src/programs/__tests__/switchboard.test.ts index 0a16a2048..d6efea02d 100644 --- a/src/agent/runner/__tests__/switchboard.test.ts +++ b/src/programs/__tests__/switchboard.test.ts @@ -2,7 +2,7 @@ * Switchboard machinery tests: binding registry lockstep, precedence chains, * trace stamping, model capabilities, and structural clamps. Per-experiment * flag behavior and cross-program isolation live in one file per experiment - * under `switchboard/flags/__tests__/`. + * under `experiments/__tests__/`. * * Every resolution test is a BindingCase: (SwitchboardCtx in) → (full * four-axis resolveBinding out), optionally pinning the trace. @@ -24,10 +24,10 @@ import { } from '@shared/constants'; import { PROGRAM_BINDINGS, - DEFAULT_BINDING, - resolveBinding, - type SwitchboardCtx, -} from '@agent/runner/switchboard'; + resolveProgramBinding as resolveBinding, +} from '../binding'; +import type { ProgramSwitchboardCtx as SwitchboardCtx } from '@programs/types'; +import { DEFAULT_AGENT_BINDING as DEFAULT_BINDING } from '@agent'; import { modelCapabilities, MINT_ALLOWED_EFFORTS, @@ -36,7 +36,7 @@ import { TRIAGE_MODELS, VALID_MODELS, } from '@agent/runner/switchboard/models'; -import { runBindingCases } from '@agent/runner/switchboard/flags/__tests__/binding-cases'; +import { runBindingCases } from '@programs/experiments/__tests__/binding-cases'; const PROGRAM_IDS = PROGRAM_REGISTRY.map((c) => c.id); const DEFAULT_RESOLVED = { diff --git a/src/agent/__tests__/variant-gating.test.ts b/src/programs/__tests__/variant-gating.test.ts similarity index 89% rename from src/agent/__tests__/variant-gating.test.ts rename to src/programs/__tests__/variant-gating.test.ts index 7efd2ca49..b0d6ed00e 100644 --- a/src/agent/__tests__/variant-gating.test.ts +++ b/src/programs/__tests__/variant-gating.test.ts @@ -1,4 +1,4 @@ -import { isOrchestratorEnabled } from '@agent/runner/switchboard'; +import { isOrchestratorEnabled } from '@programs/experiments'; describe('isOrchestratorEnabled', () => { it('is true only when the wizard-orchestrator flag is true', () => { diff --git a/src/programs/binding-telemetry.ts b/src/programs/binding-telemetry.ts new file mode 100644 index 000000000..fa6a53bef --- /dev/null +++ b/src/programs/binding-telemetry.ts @@ -0,0 +1,50 @@ +import { + Sequence, + WIZARD_ORCHESTRATOR_FLAG_KEY, + WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, +} from '@shared/constants'; +import type { ResolvedBinding } from '@agent/types'; +import { analytics } from '@utils/analytics'; +import { logToFile } from '@utils/debug'; +import type { ProgramSwitchboardCtx } from './binding'; + +/** Record the selected route and its precedence sources once per program run. */ +export function captureSwitchboardDecision( + ctx: ProgramSwitchboardCtx, + binding: ResolvedBinding, +): void { + const trace = ctx.trace ?? {}; + const perTaskModel = + binding.sequence === Sequence.orchestrator && trace.model === 'binding'; + const model = perTaskModel ? 'chosen-per-task' : binding.model; + const modelSource = perTaskModel ? 'agent-prompts' : trace.model; + analytics.wizardCapture('switchboard resolved', { + program: ctx.program, + flag_self_driving_use_pi_harness: + ctx.flags[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY], + flag_self_driving_pi_payload: JSON.stringify( + ctx.flagPayloads?.[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY] ?? null, + ), + flag_orchestrator: ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY], + cli_harness: ctx.cliHarness, + cli_sequence: ctx.cliSequence, + cli_model: ctx.cliModel, + harness_source: trace.harness, + model_source: modelSource, + sequence_source: trace.sequence, + harness: binding.harness, + model, + thinking_level: binding.thinkingLevel, + sequence: binding.sequence, + }); + logToFile( + `[switchboard] decision: program=${ctx.program}` + + ` in(orchestrator=${ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY] ?? '-'},` + + ` cli=${ctx.cliHarness ?? '-'}/${ctx.cliSequence ?? '-'}/${ + ctx.cliModel ?? '-' + })` + + ` → harness=${binding.harness} (${trace.harness ?? '?'})` + + ` model=${model} (${modelSource ?? '?'})` + + ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, + ); +} diff --git a/src/programs/binding.ts b/src/programs/binding.ts new file mode 100644 index 000000000..007058cf2 --- /dev/null +++ b/src/programs/binding.ts @@ -0,0 +1,109 @@ +import { + DEFAULT_AGENT_MODEL, + GPT5_6_SOL_MODEL, + GPT5_6_TERRA_MODEL, + Harness, + Sequence, +} from '@shared/constants'; +import { DEFAULT_AGENT_BINDING, resolveBinding, resolveHarness } from '@agent'; +import type { + ProgramBinding, + ResolvedBinding, + SwitchboardCtx, +} from '@agent/types'; +import type { ProgramId } from './program-registry'; +import { + isOrchestratorEnabled, + resolveFlagRoute, + resolveFlagSequence, +} from './experiments'; + +export interface ProgramSwitchboardCtx { + program: ProgramId; + composed?: boolean; + flags: Record; + flagPayloads?: Record; + cliHarness?: Harness; + cliSequence?: Sequence; + cliModel?: string; + trace?: SwitchboardCtx['trace']; +} + +/** Program routes. The registry lockstep contract is tested at this boundary. */ +export const PROGRAM_BINDINGS: Partial> = { + 'posthog-integration': DEFAULT_AGENT_BINDING, + 'revenue-analytics-setup': DEFAULT_AGENT_BINDING, + 'warehouse-source': DEFAULT_AGENT_BINDING, + 'error-tracking-upload-source-maps': { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_SOL_MODEL, + thinkingLevel: 'medium', + }, + audit: DEFAULT_AGENT_BINDING, + 'events-audit': DEFAULT_AGENT_BINDING, + 'posthog-doctor': DEFAULT_AGENT_BINDING, + 'web-analytics-doctor': DEFAULT_AGENT_BINDING, + migration: DEFAULT_AGENT_BINDING, + 'self-driving': DEFAULT_AGENT_BINDING, + 'agent-skill': DEFAULT_AGENT_BINDING, + 'mcp-add': DEFAULT_AGENT_BINDING, + 'mcp-remove': DEFAULT_AGENT_BINDING, + 'mcp-tutorial': DEFAULT_AGENT_BINDING, + 'mcp-analytics': DEFAULT_AGENT_BINDING, + metrics: { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, + 'replay-vision': { + sequence: Sequence.orchestrator, + harness: Harness.anthropic, + model: DEFAULT_AGENT_MODEL, + }, + 'error-tracking': { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, + 'ai-observability': { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_TERRA_MODEL, + thinkingLevel: 'high', + }, + slack: DEFAULT_AGENT_BINDING, +}; + +/** Resolve product policy once; the agent receives only the resulting route. */ +export function resolveProgramBinding( + ctx: ProgramSwitchboardCtx, +): ResolvedBinding { + ctx.trace ??= {}; + const baseBinding: ProgramBinding = + PROGRAM_BINDINGS[ctx.program] ?? DEFAULT_AGENT_BINDING; + const resolution = { + program: ctx.program, + baseBinding, + composed: ctx.composed, + flagRoute: resolveFlagRoute(ctx.program, ctx.flags, ctx.flagPayloads), + flagSequence: resolveFlagSequence(ctx.program, ctx.flags), + orchestratorFlagOn: isOrchestratorEnabled(ctx.flags), + cliHarness: ctx.cliHarness, + cliSequence: ctx.cliSequence, + cliModel: ctx.cliModel, + trace: ctx.trace, + }; + const binding = resolveBinding(resolution); + const roles = Object.keys(baseBinding.contextMillOverride ?? {}); + if (roles.length === 0) return binding; + return { + ...binding, + roleBindings: Object.fromEntries( + roles.map((role) => [ + role, + resolveHarness({ ...resolution, trace: undefined }, role), + ]), + ), + }; +} diff --git a/src/programs/commandments.ts b/src/programs/commandments.ts new file mode 100644 index 000000000..d89d267de --- /dev/null +++ b/src/programs/commandments.ts @@ -0,0 +1,18 @@ +// ── Program axis ──────────────────────────────────────────────────────── + +const SELF_DRIVING = [ + 'ALWAYS surface a custom-scout proposal in step 6b: bring the user your one or two strongest candidate scouts even when the built-in troop looks sufficient. The proposal ask leads with a "None — keep the built-in troop" option, so declining costs the user one keystroke — but a proposal you silently skip is coverage they never got to see or judge. Where the skill says to skip the ask when the gap analysis finds no candidate, do NOT skip: pick your best candidates anyway and let the user decide.', + + 'Rank candidates at the discriminator level, not the category level. "Covered" only means an enabled scout would actually FIRE for that failure mode: a conversion-rate watcher does not catch entry volume collapsing; a Stripe-transaction watcher does not catch a lead form going silent. A surface whose failure mode has no firing condition among the enabled scouts is your strongest candidate.', + + 'Be honest in the option descriptions: if a candidate overlaps something an enabled scout partially watches, say so in its description rather than dropping the candidate. The user chooses with full information; you do not gatekeep on their behalf.', +]; + +const PROGRAM_COMMANDMENTS: Record = { + 'self-driving': SELF_DRIVING, +}; + +/** Select program-specific guidance before invoking the agent. */ +export function getProgramCommandments(program: string): readonly string[] { + return PROGRAM_COMMANDMENTS[program] ?? []; +} diff --git a/src/agent/runner/switchboard/flags/__tests__/binding-cases.ts b/src/programs/experiments/__tests__/binding-cases.ts similarity index 91% rename from src/agent/runner/switchboard/flags/__tests__/binding-cases.ts rename to src/programs/experiments/__tests__/binding-cases.ts index 341a5d9a8..da7a31439 100644 --- a/src/agent/runner/switchboard/flags/__tests__/binding-cases.ts +++ b/src/programs/experiments/__tests__/binding-cases.ts @@ -5,11 +5,8 @@ */ import { describe, it, expect } from 'vitest'; import { GPT5_6_SOL_MODEL, Harness, Sequence } from '@shared/constants'; -import { - resolveBinding, - type SwitchboardCtx, - type SwitchboardTrace, -} from '@agent/runner/switchboard'; +import { resolveProgramBinding as resolveBinding } from '../../binding'; +import type { ProgramSwitchboardCtx as SwitchboardCtx } from '@programs/types'; import type { EffortLevel } from '@agent/runner/switchboard/models'; /** The complete resolved binding — every axis stated, nothing implicit. */ @@ -27,7 +24,7 @@ export interface BindingCase { ctx: Omit; binding: ExpectedBinding; /** Also pin which precedence rung decided each axis. */ - trace?: SwitchboardTrace; + trace?: SwitchboardCtx['trace']; } export function runBindingCases( diff --git a/src/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/programs/experiments/__tests__/flags.test.ts similarity index 93% rename from src/agent/runner/switchboard/flags/__tests__/flags.test.ts rename to src/programs/experiments/__tests__/flags.test.ts index 8c04f0983..e34adf86c 100644 --- a/src/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/programs/experiments/__tests__/flags.test.ts @@ -15,17 +15,14 @@ import { WIZARD_ORCHESTRATOR_OVERRIDE_FLAG_KEY, WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, } from '@shared/constants'; -import { - areSeededTasksEnabled, - resolveBinding, - resolveStageOverrides, - type SwitchboardCtx, -} from '@agent/runner/switchboard'; +import { resolveProgramBinding as resolveBinding } from '../../binding'; +import type { ProgramSwitchboardCtx as SwitchboardCtx } from '@programs/types'; +import { areSeededTasksEnabled, resolveStageOverrides } from '..'; import { ORCHESTRATOR_SEQUENCE_ROUTE, ORCHESTRATOR_HARNESS_ROUTE, -} from '@agent/runner/switchboard/flags/orchestrator'; -import { SELF_DRIVING_EXPERIMENT } from '@agent/runner/switchboard/flags/self-driving'; +} from '@programs/experiments/orchestrator'; +import { SELF_DRIVING_EXPERIMENT } from '@programs/experiments/self-driving'; import { runBindingCases } from './binding-cases'; const envState = vi.hoisted(() => ({ @@ -332,22 +329,17 @@ describe('isolation — everything on at once', () => { }); }); -describe('seam scan — routing reads live only in flags/', () => { - const switchboardDir = join( - dirname(fileURLToPath(import.meta.url)), - '..', - '..', - ); - // orchestrator-runner consumes a flags/ resolver; it may pass the snapshot through, never index it. +describe('seam scan — routing reads live only in experiments/', () => { + const programsDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); for (const file of [ - 'harness.ts', - 'sequence.ts', - 'models.ts', - 'index.ts', - '../sequence/orchestrator/orchestrator-runner.ts', + '../agent/runner/switchboard/harness.ts', + '../agent/runner/switchboard/sequence.ts', + '../agent/runner/switchboard/models.ts', + '../agent/runner/switchboard/index.ts', + '../agent/runner/sequence/orchestrator/orchestrator-runner.ts', ]) { it(`${file} contains no direct flag reads or flag-key imports`, () => { - const src = readFileSync(join(switchboardDir, file), 'utf8'); + const src = readFileSync(join(programsDir, file), 'utf8'); expect(src).not.toMatch(/ctx\.flags\[/); expect(src).not.toMatch(/flags\[['"`]/); expect(src).not.toMatch(/WIZARD_\w+_FLAG_KEY/); diff --git a/src/agent/runner/switchboard/flags/index.ts b/src/programs/experiments/index.ts similarity index 100% rename from src/agent/runner/switchboard/flags/index.ts rename to src/programs/experiments/index.ts diff --git a/src/agent/runner/switchboard/flags/orchestrator.ts b/src/programs/experiments/orchestrator.ts similarity index 100% rename from src/agent/runner/switchboard/flags/orchestrator.ts rename to src/programs/experiments/orchestrator.ts diff --git a/src/agent/runner/switchboard/flags/schemes.ts b/src/programs/experiments/schemes.ts similarity index 99% rename from src/agent/runner/switchboard/flags/schemes.ts rename to src/programs/experiments/schemes.ts index 99f9c4a97..1fd30ce34 100644 --- a/src/agent/runner/switchboard/flags/schemes.ts +++ b/src/programs/experiments/schemes.ts @@ -15,7 +15,7 @@ import { } from '@shared/constants'; import type { ProgramId } from '@programs/types'; import { logToFile } from '@utils/debug'; -import type { EffortLevel } from '../models'; +import type { EffortLevel } from '@agent/types'; // ── Shared vocabulary ───────────────────────────────────────────────────── diff --git a/src/agent/runner/switchboard/flags/self-driving.ts b/src/programs/experiments/self-driving.ts similarity index 100% rename from src/agent/runner/switchboard/flags/self-driving.ts rename to src/programs/experiments/self-driving.ts diff --git a/src/programs/run-agent-legacy.ts b/src/programs/run-agent-legacy.ts index b343cccf6..8e2174986 100644 --- a/src/programs/run-agent-legacy.ts +++ b/src/programs/run-agent-legacy.ts @@ -21,17 +21,15 @@ import { createUiReducer, uiInteraction } from '@ui/agent-progress'; import { buildRunTags, flushScanReport, - resolveBinding, runAgent, RunOutcome, TASK_OUTCOMES_KEY, } from '@agent'; -import type { - ProgramBinding, - RunConfig, - RunInput, - SwitchboardCtx, -} from '@agent/types'; +import type { RunConfig, RunInput } from '@agent/types'; +import { resolveProgramBinding, type ProgramSwitchboardCtx } from './binding'; +import { getProgramCommandments } from './commandments'; +import { captureSwitchboardDecision } from './binding-telemetry'; +import { areSeededTasksEnabled, resolveStageOverrides } from './experiments'; import type { ProgramRun } from './program-run'; import { backupAndFixClaudeSettings, @@ -53,8 +51,6 @@ import { isNonInteractiveEnvironment } from '@utils/environment'; import { getSkillsBaseUrl, Sequence, - WIZARD_ORCHESTRATOR_FLAG_KEY, - WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, type Integration, } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@programs/registry'; @@ -181,7 +177,7 @@ async function runProgram( // Resolve which sequence and harness will run a program (CLI → PostHog flag → // per-program binding → default), tag both axes onto analytics, and hand the // binding to the agent for dispatch. - const switchboard: SwitchboardCtx = { + const switchboard: ProgramSwitchboardCtx = { program: programConfig.id, composed, flags: wizardFlags, @@ -190,7 +186,7 @@ async function runProgram( cliSequence: session.sequence, cliModel: session.model, }; - const binding = resolveBinding(switchboard); + const binding = resolveProgramBinding(switchboard); analytics.setTag('sequence', binding.sequence); analytics.setTag('harness', binding.harness); wizardMetadata.SEQUENCE = binding.sequence; @@ -221,7 +217,13 @@ async function runProgram( run, composed, binding, - switchboard, + programCommandments: getProgramCommandments(programConfig.id), + stageOverrides: resolveStageOverrides( + programConfig.id, + wizardFlags, + wizardFlagPayloads, + ), + seededTasksEnabled: areSeededTasksEnabled(wizardFlags), skillsBaseUrl: getSkillsBaseUrl(), wizardFlags, wizardFlagPayloads, @@ -440,50 +442,3 @@ async function runSettingsGate(session: WizardSession): Promise { logToFile('[agent-runner] settings override resolved'); } } - -// ── Switchboard telemetry ───────────────────────────────────────────── - -/** - * One event + one log line per run: what entered the switchboard, which - * precedence rung decided each axis, and the final pick. - */ -function captureSwitchboardDecision( - ctx: SwitchboardCtx, - binding: ProgramBinding, -): void { - const trace = ctx.trace ?? {}; - // Unpinned orchestrator runs choose a model per task from the context-mill agent prompts; the orchestrator logs that map once the prompts load. - const perTaskModel = - binding.sequence === Sequence.orchestrator && trace.model === 'binding'; - const model = perTaskModel ? 'chosen-per-task' : binding.model; - const modelSource = perTaskModel ? 'agent-prompts' : trace.model; - analytics.wizardCapture('switchboard resolved', { - program: ctx.program, - flag_self_driving_use_pi_harness: - ctx.flags[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY], - flag_self_driving_pi_payload: JSON.stringify( - ctx.flagPayloads?.[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY] ?? null, - ), - flag_orchestrator: ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY], - cli_harness: ctx.cliHarness, - cli_sequence: ctx.cliSequence, - cli_model: ctx.cliModel, - harness_source: trace.harness, - model_source: modelSource, - sequence_source: trace.sequence, - harness: binding.harness, - model, - thinking_level: binding.thinkingLevel, - sequence: binding.sequence, - }); - logToFile( - `[switchboard] decision: program=${ctx.program}` + - ` in(orchestrator=${ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY] ?? '-'},` + - ` cli=${ctx.cliHarness ?? '-'}/${ctx.cliSequence ?? '-'}/${ - ctx.cliModel ?? '-' - })` + - ` → harness=${binding.harness} (${trace.harness ?? '?'})` + - ` model=${model} (${modelSource ?? '?'})` + - ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, - ); -} diff --git a/src/programs/types.ts b/src/programs/types.ts index 620e94d18..5ac379afe 100644 --- a/src/programs/types.ts +++ b/src/programs/types.ts @@ -7,3 +7,4 @@ export type { StoreInitContext, } from './program-step'; export type { FrameworkConfig, SetupQuestion } from './framework-config'; +export type { ProgramSwitchboardCtx } from './binding'; From 09c681b9bd629cfa1e7edd0cb87fffd9750bf34b Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 15:43:04 -0400 Subject: [PATCH 55/61] refactor(agent): move the harness resolver into resolve-harness resolveHarness and resolveRoleHarness move from switchboard/harness.ts to switchboard/resolve-harness.ts, so harness.ts holds only the registry. The three-step middleware chain becomes straight-line code with the same precedence: binding, then the flag route, then the dev-build CLI model and harness overrides. The traces and log line are unchanged. HARNESS_RUNS_TASKS records which backends implement runTask, and the sequence capability clamp reads it through harnessRunsTasks instead of asking the live registry. Both backends implement runTask, so the clamp decides the same way. A registry test keeps the table in step with HARNESS_OPTIONS. The DEFAULT_BINDING alias goes, and the sequence resolver reads DEFAULT_AGENT_BINDING. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__tests__/harness-capabilities.test.ts | 16 +++ .../__tests__/run-agent-standalone.test.ts | 10 -- src/agent/index.ts | 2 +- src/agent/runner/switchboard/harness.ts | 105 +----------------- src/agent/runner/switchboard/index.ts | 14 +-- .../runner/switchboard/resolve-harness.ts | 90 +++++++++++++++ src/agent/runner/switchboard/sequence.ts | 18 ++- 7 files changed, 123 insertions(+), 132 deletions(-) create mode 100644 src/agent/__tests__/harness-capabilities.test.ts create mode 100644 src/agent/runner/switchboard/resolve-harness.ts diff --git a/src/agent/__tests__/harness-capabilities.test.ts b/src/agent/__tests__/harness-capabilities.test.ts new file mode 100644 index 000000000..d491f90d0 --- /dev/null +++ b/src/agent/__tests__/harness-capabilities.test.ts @@ -0,0 +1,16 @@ +import { Harness } from '@shared/constants'; +import { HARNESS_OPTIONS } from '@agent/runner/switchboard/harness'; +import { HARNESS_RUNS_TASKS } from '@agent/runner/switchboard/resolve-harness'; + +describe('harness capabilities', () => { + it.each(Object.values(Harness))( + 'records whether the %s backend implements runTask', + (harness) => { + const backend = HARNESS_OPTIONS[harness]; + expect(backend).toBeDefined(); + expect(HARNESS_RUNS_TASKS[harness]).toBe( + typeof backend?.runTask === 'function', + ); + }, + ); +}); diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index 19e75663a..24c3f39a8 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -171,16 +171,6 @@ vi.mock('@agent/runner/switchboard/harness', () => { harnessState.selected.push(name); return { ...fake, name }; }, - resolveHarness: (ctx: { cliHarness?: Harness }) => ({ - harness: ctx.cliHarness ?? Harness.pi, - model: DEFAULT_AGENT_MODEL, - }), - resolveRoleHarness: (binding: RunConfig['binding'], role: string) => - binding.roleBindings?.[role] ?? { - harness: binding.harness, - model: binding.model, - thinkingLevel: binding.thinkingLevel, - }, }; }); diff --git a/src/agent/index.ts b/src/agent/index.ts index 577ffa776..7b2c47553 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -19,7 +19,7 @@ export { runAgent, RunOutcome } from './runner'; export { AgentSignals } from './agent-interface'; export { WIZARD_TOOL_NAMES } from './tools'; export { DEFAULT_AGENT_BINDING } from './default-binding'; -export { resolveHarness } from './runner/switchboard'; +export { resolveHarness } from './runner/switchboard/resolve-harness'; /** * Leaves in B2. resolveBinding applies the generic precedence to the base diff --git a/src/agent/runner/switchboard/harness.ts b/src/agent/runner/switchboard/harness.ts index edfac1b59..b9078ac34 100644 --- a/src/agent/runner/switchboard/harness.ts +++ b/src/agent/runner/switchboard/harness.ts @@ -1,20 +1,12 @@ /** - * Harness axis: registry, middleware, resolver. Mirrors `sequence.ts`. + * Harness axis: the registry. Mirrors `sequence.ts`; the resolver is + * `resolve-harness.ts`. */ -import { IS_PRODUCTION_BUILD } from '@env'; import { Harness } from '@shared/constants'; -import { logToFile } from '@utils/debug'; import { anthropicBackend } from '../harness/anthropic'; import { piBackend } from '../harness/pi'; import type { AgentHarness } from '../harness/types'; -import { - DEFAULT_BINDING, - runChain, - type HarnessPick, - type Middleware, - type SwitchboardCtx, -} from '.'; export const HARNESS_OPTIONS: Partial> = { [Harness.anthropic]: anthropicBackend, @@ -28,96 +20,3 @@ export function getHarness(name: Harness): AgentHarness { } return harness; } - -/** - * A validated caller-supplied route overlays the base binding. - */ -const flagRunnerOverride: Middleware = (ctx, next) => { - const pick = next(); - const route = ctx.flagRoute; - if (!route) return pick; - if (ctx.trace) { - ctx.trace.harness = 'flag'; - // Harness-only routes keep the binding's model — trace it truthfully so - // analytics never attributes the fallback model to the flag. - if (route.model) ctx.trace.model = 'flag'; - } - return { - harness: route.harness ?? Harness.pi, - model: route.model ?? pick.model, - thinkingLevel: route.thinkingLevel ?? pick.thinkingLevel, - }; -}; - -/** `--harness` override. Dev/test only — the option is gated out of published builds. */ -const cliHarnessOverride: Middleware = (ctx, next) => { - const pick = next(); - if (!ctx.cliHarness) return pick; - if (ctx.trace) ctx.trace.harness = 'cli'; - return { ...pick, harness: ctx.cliHarness }; -}; - -/** `--model` override. Dev/test only — the option is gated out of published builds. */ -const cliModelOverride: Middleware = (ctx, next) => { - const pick = next(); - if (!ctx.cliModel) return pick; - if (ctx.trace) ctx.trace.model = 'cli'; - return { ...pick, model: ctx.cliModel }; -}; - -// Order = precedence: CLI > flag > binding default. The prod spread collapses -// to [], dropping the CLI overrides from the chain. -const HARNESS_MIDDLEWARE: Middleware[] = [ - ...(IS_PRODUCTION_BUILD ? [] : [cliHarnessOverride, cliModelOverride]), - flagRunnerOverride, -]; - -/** - * Resolve the harness for a role. Linear callers omit `role`; orchestrator - * callers pass `'seed'` or `task.type`. `contextMillOverride[role]` overlays. - */ -export function resolveHarness( - ctx: SwitchboardCtx, - role = 'default', -): HarnessPick { - const pick = runChain(HARNESS_MIDDLEWARE, ctx, () => { - if (ctx.trace) - Object.assign(ctx.trace, { harness: 'binding', model: 'binding' }); - const binding = ctx.baseBinding ?? DEFAULT_BINDING; - return { - harness: binding.harness, - model: binding.model, - thinkingLevel: binding.thinkingLevel, - ...binding.contextMillOverride?.[role], - }; - }); - logToFile( - `[switchboard] resolved: program=${ctx.program ?? '?'} harness=${ - pick.harness - }` + - `${ctx.trace?.harness ? ` (${ctx.trace.harness})` : ''} model=${ - pick.model - }` + - `${ctx.trace?.model ? ` (${ctx.trace.model})` : ''}`, - ); - return pick; -} - -/** The agent resolves a task role only from data the caller already supplied. */ -export function resolveRoleHarness( - binding: { - harness: Harness; - model: string; - thinkingLevel?: HarnessPick['thinkingLevel']; - roleBindings?: Record; - }, - role: string, -): HarnessPick { - return ( - binding.roleBindings?.[role] ?? { - harness: binding.harness, - model: binding.model, - thinkingLevel: binding.thinkingLevel, - } - ); -} diff --git a/src/agent/runner/switchboard/index.ts b/src/agent/runner/switchboard/index.ts index a1a668ee0..aff3b7716 100644 --- a/src/agent/runner/switchboard/index.ts +++ b/src/agent/runner/switchboard/index.ts @@ -1,9 +1,8 @@ // Resolves routing; model additions also require mint allowlists and gateway prompt/transport support. import { Harness, Sequence } from '@shared/constants'; -import { DEFAULT_AGENT_BINDING } from '@agent/default-binding'; -import { resolveHarness } from './harness'; import type { EffortLevel } from './models'; +import { resolveHarness } from './resolve-harness'; import { resolveSequence } from './sequence'; // ── Shared machinery ──────────────────────────────────────────────────── @@ -106,9 +105,6 @@ export interface ProgramBinding { contextMillOverride?: Record>; } -/** Legacy alias until the public runner export is removed in B2 integration. */ -export const DEFAULT_BINDING: ProgramBinding = DEFAULT_AGENT_BINDING; - // ── Unified resolver ──────────────────────────────────────────────────── /** Compose both axes. Callers needing only one axis use the per-axis resolver. */ @@ -123,11 +119,15 @@ export function resolveBinding( } // ── Unified re-export surface ─────────────────────────────────────────── -export { HARNESS_OPTIONS, getHarness, resolveHarness } from './harness'; +export { HARNESS_OPTIONS, getHarness } from './harness'; +export { + harnessRunsTasks, + resolveHarness, + resolveRoleHarness, +} from './resolve-harness'; export { SEQUENCE_OPTIONS, getSequence, resolveSequence, type SequenceRunner, } from './sequence'; -export { resolveRoleHarness } from './harness'; diff --git a/src/agent/runner/switchboard/resolve-harness.ts b/src/agent/runner/switchboard/resolve-harness.ts new file mode 100644 index 000000000..af47f4fa5 --- /dev/null +++ b/src/agent/runner/switchboard/resolve-harness.ts @@ -0,0 +1,90 @@ +/** + * Harness axis: the resolver that picks a harness and model, and which + * harnesses the orchestrator can drive. Data and pure functions only, so the + * agent entry loads this at startup; the registry is `harness.ts`. + */ + +import { IS_PRODUCTION_BUILD } from '@env'; +import { Harness } from '@shared/constants'; +import { logToFile } from '@utils/debug'; +import { DEFAULT_AGENT_BINDING } from '@agent/default-binding'; +import type { ResolvedBinding } from '../shared/types'; +import type { HarnessPick, ProgramBinding, SwitchboardCtx } from '.'; + +/** Which backends implement `runTask`; a registry test keeps this in step with HARNESS_OPTIONS. */ +export const HARNESS_RUNS_TASKS: Record = { + [Harness.anthropic]: true, + [Harness.pi]: true, +}; + +/** Whether the orchestrator can drive this harness. */ +export function harnessRunsTasks(name: Harness): boolean { + return HARNESS_RUNS_TASKS[name] === true; +} + +/** + * Resolve the harness for a role. Linear callers omit `role`; orchestrator + * callers pass `'seed'` or `task.type`. `contextMillOverride[role]` overlays. + * Precedence: CLI > flag route > binding default; the CLI overrides are + * dev/test only and gated out of published builds. + */ +export function resolveHarness( + ctx: SwitchboardCtx, + role = 'default', +): HarnessPick { + if (ctx.trace) + Object.assign(ctx.trace, { harness: 'binding', model: 'binding' }); + const binding: ProgramBinding = ctx.baseBinding ?? DEFAULT_AGENT_BINDING; + let pick: HarnessPick = { + harness: binding.harness, + model: binding.model, + thinkingLevel: binding.thinkingLevel, + ...binding.contextMillOverride?.[role], + }; + const route = ctx.flagRoute; + if (route) { + if (ctx.trace) { + ctx.trace.harness = 'flag'; + // Harness-only routes keep the binding's model — trace it truthfully so + // analytics never attributes the fallback model to the flag. + if (route.model) ctx.trace.model = 'flag'; + } + pick = { + harness: route.harness ?? Harness.pi, + model: route.model ?? pick.model, + thinkingLevel: route.thinkingLevel ?? pick.thinkingLevel, + }; + } + if (!IS_PRODUCTION_BUILD && ctx.cliModel) { + if (ctx.trace) ctx.trace.model = 'cli'; + pick = { ...pick, model: ctx.cliModel }; + } + if (!IS_PRODUCTION_BUILD && ctx.cliHarness) { + if (ctx.trace) ctx.trace.harness = 'cli'; + pick = { ...pick, harness: ctx.cliHarness }; + } + logToFile( + `[switchboard] resolved: program=${ctx.program ?? '?'} harness=${ + pick.harness + }` + + `${ctx.trace?.harness ? ` (${ctx.trace.harness})` : ''} model=${ + pick.model + }` + + `${ctx.trace?.model ? ` (${ctx.trace.model})` : ''}`, + ); + return pick; +} + +/** The agent resolves a task role only from data the caller already supplied. */ +export function resolveRoleHarness( + binding: ResolvedBinding, + role: string, +): HarnessPick { + return ( + binding.roleBindings?.[role] ?? { + harness: binding.harness, + model: binding.model, + thinkingLevel: binding.thinkingLevel, + } + ); +} diff --git a/src/agent/runner/switchboard/sequence.ts b/src/agent/runner/switchboard/sequence.ts index 09af8843a..8b3eb7792 100644 --- a/src/agent/runner/switchboard/sequence.ts +++ b/src/agent/runner/switchboard/sequence.ts @@ -5,16 +5,12 @@ import { IS_PRODUCTION_BUILD } from '@env'; import { Sequence } from '@shared/constants'; import { logToFile } from '@utils/debug'; -import { getHarness, resolveHarness } from './harness'; +import { DEFAULT_AGENT_BINDING } from '@agent/default-binding'; +import { harnessRunsTasks, resolveHarness } from './resolve-harness'; import type { SequenceResult, SequenceContext } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; -import { - DEFAULT_BINDING, - runChain, - type Middleware, - type SwitchboardCtx, -} from '.'; +import { runChain, type Middleware, type SwitchboardCtx } from '.'; // ── Registry ──────────────────────────────────────────────────────────── @@ -82,13 +78,13 @@ const sequenceExperimentMw: Middleware = (ctx, next) => { /** * The orchestrator drives harnesses through `runTask`; a harness that has not * implemented it clamps the run to linear. A capability check, not a harness - * identity check — a harness gains orchestrator support by implementing the - * method, with no switchboard change. Sits below the CLI override so + * identity check — `HARNESS_RUNS_TASKS` records which backends implement the + * method, and a registry test keeps it in step. Sits below the CLI override so * `--sequence orchestrator` still reproduces the hard error in dev builds. */ const runTaskCapabilityClampMw: Middleware = (ctx, next) => { const pick = resolveHarness(ctx); - if (getHarness(pick.harness).runTask) return next(); + if (harnessRunsTasks(pick.harness)) return next(); if (ctx.orchestratorFlagOn) { logToFile( `[switchboard] wizard-orchestrator ignored: ${pick.harness} has no runTask, clamping to linear`, @@ -112,7 +108,7 @@ const SEQUENCE_MIDDLEWARE: Middleware[] = [ export function resolveSequence(ctx: SwitchboardCtx): Sequence { const sequence = runChain(SEQUENCE_MIDDLEWARE, ctx, () => { if (ctx.trace) ctx.trace.sequence = 'binding'; - const binding = ctx.baseBinding ?? DEFAULT_BINDING; + const binding = ctx.baseBinding ?? DEFAULT_AGENT_BINDING; return binding.sequence; }); logToFile( From ef5ee439bf9186f7b8d0414c30fd84f1d6a2f9d2 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 15:45:40 -0400 Subject: [PATCH 56/61] refactor(programs): move the sequence precedence into binding resolveProgramBinding now resolves the sequence itself, so programs own the whole binding precedence. The agent keeps the harness axis. The middleware chain becomes straight-line code with the same order: the composed clamp, the dev-build CLI override, the runTask capability clamp, the flag route, the sequence experiment, then the base binding. The traces and log lines are unchanged. The agent's switchboard keeps the registries, resolveHarness, resolveRoleHarness and harnessRunsTasks. It drops resolveBinding, resolveSequence, runChain, and the flagSequence and orchestratorFlagOn context fields, which only programs read. @agent exports harnessRunsTasks beside resolveHarness in place of resolveBinding. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/agent/README.md | 2 +- src/agent/index.ts | 15 ++-- src/agent/runner/README.md | 17 ++--- src/agent/runner/index.ts | 1 - src/agent/runner/switchboard/index.ts | 60 +--------------- src/agent/runner/switchboard/sequence.ts | 88 +----------------------- src/agent/types.ts | 2 +- src/programs/binding.ts | 57 ++++++++++++++- 8 files changed, 77 insertions(+), 165 deletions(-) diff --git a/src/agent/README.md b/src/agent/README.md index a3c188057..90352d0cf 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -56,7 +56,7 @@ runAgent(config: RunConfig, input: RunInput, options?: { is `success`, `Aborted` is `cancelled`, `Failed` and `Crashed` are `error`. Other runtime exports: `DEFAULT_AGENT_BINDING` for standalone callers, -`resolveHarness` and `resolveBinding`, which programs resolve a binding with, +`resolveHarness` and `harnessRunsTasks`, which programs resolve a binding with, `shouldDisableAsk`, `initializeAgent`, `executeAgent`, `buildRunTags`, `AgentSignals`, `configureGatewayFromCIEnvironment`, `downloadSkill`, `WIZARD_TOOL_NAMES`, `LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and diff --git a/src/agent/index.ts b/src/agent/index.ts index 7b2c47553..2878fa23c 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -12,22 +12,23 @@ * Stays. The agent's contract: the one way to run it, the marker strings * program prompts embed, the tool ids programs put in allowedTools and * disallowedTools, and what programs resolve a binding with: the default - * binding and the harness axis. + * binding, the harness axis and each harness's task capability. */ export type * from './types'; export { runAgent, RunOutcome } from './runner'; export { AgentSignals } from './agent-interface'; export { WIZARD_TOOL_NAMES } from './tools'; export { DEFAULT_AGENT_BINDING } from './default-binding'; -export { resolveHarness } from './runner/switchboard/resolve-harness'; +export { + harnessRunsTasks, + resolveHarness, +} from './runner/switchboard/resolve-harness'; /** - * Leaves in B2. resolveBinding applies the generic precedence to the base - * binding and routes programs select, and programs/binding.ts is its one - * caller; shouldDisableAsk is a flags policy programs decide and pass in; - * LONGER_ASK_TIMEOUT_MS is a tuning number programs own as askTimeoutMs. + * Leaves in B2. shouldDisableAsk is a flags policy programs decide and pass + * in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own as askTimeoutMs. */ -export { resolveBinding, shouldDisableAsk } from './runner'; +export { shouldDisableAsk } from './runner'; export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; /** diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index 611be4c79..afd87744d 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -52,14 +52,15 @@ The gates, OAuth, flags and binding lookup that used to run here live in targets, the gateway mint and the scan-triage classifier. Whether the run turns out to be linear or orchestrator, anthropic or pi, the setup is the same. -**The switchboard** (`switchboard/`) is the router. Given a program id + the -fetched flags + any CLI overrides, it returns a `ProgramBinding` — which query -shape (sequence), which agent SDK (harness), which model. Two independent -middleware chains, one per axis, apply precedence rules (CLI > flag > program -config > default). This is the only layer that makes routing decisions. - -**Sequences** (`sequence/`) are LLM query shapes. Once the switchboard has -picked one, that sequence takes over the run and owns _how the LLM's work is +**The switchboard** (`switchboard/`) holds the sequence and harness registries +and the harness axis. Programs resolve the binding (`resolveProgramBinding`): +the sequence precedence lives there, and the harness and model come from this +layer's `resolveHarness` (CLI > flag > program config > +default). `harnessRunsTasks` tells programs which harnesses the orchestrator can +drive. + +**Sequences** (`sequence/`) are LLM query shapes. Once the binding has picked +one, that sequence takes over the run and owns _how the LLM's work is shaped_. See `sequence/README.md`. - **linear** — one long conversation with the model, start to finish. diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index 91d35dedc..0544bffb3 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -62,7 +62,6 @@ export type { ProgressEmitter, } from '@agent/progress'; export { shouldDisableAsk } from './shared/bootstrap'; -export { resolveBinding } from './switchboard'; export type { ProgramBinding, SwitchboardCtx } from './switchboard'; export { TASK_OUTCOMES_KEY } from './sequence/orchestrator/queue'; export type { TaskOutcome } from './sequence/orchestrator/queue'; diff --git a/src/agent/runner/switchboard/index.ts b/src/agent/runner/switchboard/index.ts index aff3b7716..7b6203f81 100644 --- a/src/agent/runner/switchboard/index.ts +++ b/src/agent/runner/switchboard/index.ts @@ -2,12 +2,10 @@ import { Harness, Sequence } from '@shared/constants'; import type { EffortLevel } from './models'; -import { resolveHarness } from './resolve-harness'; -import { resolveSequence } from './sequence'; // ── Shared machinery ──────────────────────────────────────────────────── -/** Which precedence rung decided each axis. Stamped by middlewares as they assert. */ +/** Which precedence rung decided each axis. Stamped by the resolvers as they decide. */ export interface SwitchboardTrace { harness?: 'cli' | 'flag' | 'binding'; model?: 'cli' | 'flag' | 'binding'; @@ -20,7 +18,7 @@ export interface SwitchboardTrace { | 'binding'; } -/** Everything a resolver middleware may branch on. Built once per run. */ +/** Everything a resolver may branch on. Built once per run. */ export interface SwitchboardCtx { /** Opaque log label. Program lookup stays with the caller. */ program?: string; @@ -35,9 +33,6 @@ export interface SwitchboardCtx { thinkingLevel?: EffortLevel; sequence?: Sequence; }; - flagSequence?: Sequence; - /** Raw boolean only for the existing capability-clamp log line. */ - orchestratorFlagOn?: boolean; /** CLI override (`--harness`). Wins over `flags`. */ cliHarness?: Harness; /** CLI override (`--sequence`). Wins over `flags`. */ @@ -48,37 +43,6 @@ export interface SwitchboardCtx { trace?: SwitchboardTrace; } -/** A resolver middleware: defer via `next()`, or assert by returning a value. */ -export type Middleware = (ctx: SwitchboardCtx, next: () => D) => D; - -/** - * Run a middleware chain over `ctx`. Each middleware receives `next` (which - * runs the rest of the chain) and can either: - * - defer: call `next()` and optionally modify its result (overlay pattern) - * - short-circuit: return a value without calling `next()` (skip the rest) - * - * **Earlier in the array = higher precedence.** Index 0 runs first and can - * short-circuit the rest; index 1 only runs if index 0 deferred. So - * `[cliSequenceMw, orchestratorFeatureFlagMw]` means CLI takes precedence over the - * flag, not the other way around. - * - * `fallback` runs at the end — reached only when every middleware deferred. - * Typically the map read for the base value. - */ -export function runChain( - chain: Middleware[], - ctx: SwitchboardCtx, - fallback: () => D, -): D { - function step(index: number): D { - if (index >= chain.length) return fallback(); - const middleware = chain[index]; - const next = () => step(index + 1); - return middleware(ctx, next); - } - return step(0); -} - // ── Data model ────────────────────────────────────────────────────────── /** Harness + model for one leaf of agent work. */ @@ -105,19 +69,6 @@ export interface ProgramBinding { contextMillOverride?: Record>; } -// ── Unified resolver ──────────────────────────────────────────────────── - -/** Compose both axes. Callers needing only one axis use the per-axis resolver. */ -export function resolveBinding( - ctx: SwitchboardCtx, - role = 'default', -): ProgramBinding { - ctx.trace ??= {}; - const sequence = resolveSequence(ctx); - const { harness, model, thinkingLevel } = resolveHarness(ctx, role); - return { sequence, harness, model, thinkingLevel }; -} - // ── Unified re-export surface ─────────────────────────────────────────── export { HARNESS_OPTIONS, getHarness } from './harness'; export { @@ -125,9 +76,4 @@ export { resolveHarness, resolveRoleHarness, } from './resolve-harness'; -export { - SEQUENCE_OPTIONS, - getSequence, - resolveSequence, - type SequenceRunner, -} from './sequence'; +export { SEQUENCE_OPTIONS, getSequence, type SequenceRunner } from './sequence'; diff --git a/src/agent/runner/switchboard/sequence.ts b/src/agent/runner/switchboard/sequence.ts index 8b3eb7792..43881f268 100644 --- a/src/agent/runner/switchboard/sequence.ts +++ b/src/agent/runner/switchboard/sequence.ts @@ -1,16 +1,11 @@ /** - * Sequence axis: registry, clamps and generic resolution of caller-supplied policy. + * Sequence axis: the registry. Programs resolve which sequence a run uses. */ -import { IS_PRODUCTION_BUILD } from '@env'; import { Sequence } from '@shared/constants'; -import { logToFile } from '@utils/debug'; -import { DEFAULT_AGENT_BINDING } from '@agent/default-binding'; -import { harnessRunsTasks, resolveHarness } from './resolve-harness'; import type { SequenceResult, SequenceContext } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; -import { runChain, type Middleware, type SwitchboardCtx } from '.'; // ── Registry ──────────────────────────────────────────────────────────── @@ -38,84 +33,3 @@ export function getSequence(name: Sequence): SequenceRunner { } return sequence; } - -// ── Middleware + resolver ─────────────────────────────────────────────── - -/** - * A composed sub-run (integration inside self-driving) is structurally - * linear: the orchestrator owns the full run lifecycle (queue, outro) and - * cannot nest. Sits above every override, including CLI. - */ -const composedClampMw: Middleware = (ctx, next) => { - if (!ctx.composed) return next(); - if (ctx.trace) ctx.trace.sequence = 'composed'; - return Sequence.linear; -}; - -/** `--sequence` override. Dev/test only — the option is gated out of published builds. */ -const cliSequenceMw: Middleware = (ctx, next) => { - if (!ctx.cliSequence) return next(); - if (ctx.trace) ctx.trace.sequence = 'cli'; - return ctx.cliSequence; -}; - -/** A caller-supplied payload route may pin the sequence. */ -const flagRouteSequenceMw: Middleware = (ctx, next) => { - const route = ctx.flagRoute; - if (!route?.sequence) return next(); - if (ctx.trace) ctx.trace.sequence = 'payload'; - return route.sequence; -}; - -/** A caller-supplied sequence experiment, already scoped to its program. */ -const sequenceExperimentMw: Middleware = (ctx, next) => { - const sequence = ctx.flagSequence; - if (!sequence) return next(); - if (ctx.trace) ctx.trace.sequence = 'flag'; - return sequence; -}; - -/** - * The orchestrator drives harnesses through `runTask`; a harness that has not - * implemented it clamps the run to linear. A capability check, not a harness - * identity check — `HARNESS_RUNS_TASKS` records which backends implement the - * method, and a registry test keeps it in step. Sits below the CLI override so - * `--sequence orchestrator` still reproduces the hard error in dev builds. - */ -const runTaskCapabilityClampMw: Middleware = (ctx, next) => { - const pick = resolveHarness(ctx); - if (harnessRunsTasks(pick.harness)) return next(); - if (ctx.orchestratorFlagOn) { - logToFile( - `[switchboard] wizard-orchestrator ignored: ${pick.harness} has no runTask, clamping to linear`, - ); - } - if (ctx.trace) ctx.trace.sequence = 'runtask-clamp'; - return Sequence.linear; -}; - -// Order = precedence: CLI > capability clamp > flag > binding default. The -// prod spread collapses to [], dropping cliSequenceMw from the chain. -const SEQUENCE_MIDDLEWARE: Middleware[] = [ - composedClampMw, - ...(IS_PRODUCTION_BUILD ? [] : [cliSequenceMw]), - runTaskCapabilityClampMw, - flagRouteSequenceMw, - sequenceExperimentMw, -]; - -/** CLI wins over `wizard-orchestrator` flag wins over binding default. */ -export function resolveSequence(ctx: SwitchboardCtx): Sequence { - const sequence = runChain(SEQUENCE_MIDDLEWARE, ctx, () => { - if (ctx.trace) ctx.trace.sequence = 'binding'; - const binding = ctx.baseBinding ?? DEFAULT_AGENT_BINDING; - return binding.sequence; - }); - logToFile( - `[switchboard] resolved: program=${ - ctx.program ?? '?' - } sequence=${sequence}` + - `${ctx.trace?.sequence ? ` (${ctx.trace.sequence})` : ''}`, - ); - return sequence; -} diff --git a/src/agent/types.ts b/src/agent/types.ts index 2b4bd4352..94bce6df4 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -31,7 +31,7 @@ export type { TokenUsageDelta, } from './progress'; -/** Input types of the exported resolveBinding and resolveHarness. */ +/** Input types of the exported resolveHarness. */ export type { ProgramBinding, SwitchboardCtx } from './runner'; export type { EffortLevel } from './runner/switchboard/models'; diff --git a/src/programs/binding.ts b/src/programs/binding.ts index 007058cf2..59c30b348 100644 --- a/src/programs/binding.ts +++ b/src/programs/binding.ts @@ -1,3 +1,4 @@ +import { IS_PRODUCTION_BUILD } from '@env'; import { DEFAULT_AGENT_MODEL, GPT5_6_SOL_MODEL, @@ -5,11 +6,16 @@ import { Harness, Sequence, } from '@shared/constants'; -import { DEFAULT_AGENT_BINDING, resolveBinding, resolveHarness } from '@agent'; +import { logToFile } from '@utils/debug'; +import { + DEFAULT_AGENT_BINDING, + harnessRunsTasks, + resolveHarness, +} from '@agent'; import type { ProgramBinding, ResolvedBinding, - SwitchboardCtx, + SwitchboardCtx as HarnessCtx, } from '@agent/types'; import type { ProgramId } from './program-registry'; import { @@ -18,6 +24,12 @@ import { resolveFlagSequence, } from './experiments'; +/** The agent's harness inputs plus the sequence inputs only programs read. */ +type SwitchboardCtx = HarnessCtx & { + flagSequence?: Sequence; + orchestratorFlagOn?: boolean; +}; + export interface ProgramSwitchboardCtx { program: ProgramId; composed?: boolean; @@ -94,7 +106,9 @@ export function resolveProgramBinding( cliModel: ctx.cliModel, trace: ctx.trace, }; - const binding = resolveBinding(resolution); + const sequence = resolveSequence(resolution); + const { harness, model, thinkingLevel } = resolveHarness(resolution); + const binding = { sequence, harness, model, thinkingLevel }; const roles = Object.keys(baseBinding.contextMillOverride ?? {}); if (roles.length === 0) return binding; return { @@ -107,3 +121,40 @@ export function resolveProgramBinding( ), }; } + +function resolveSequence(ctx: SwitchboardCtx): Sequence { + const [source, sequence] = pickSequence(ctx); + if (ctx.trace) ctx.trace.sequence = source; + logToFile( + `[switchboard] resolved: program=${ + ctx.program ?? '?' + } sequence=${sequence} (${source})`, + ); + return sequence; +} + +/** + * The first rung that decides wins: the composed clamp, the dev-build CLI + * override, the runTask capability clamp, the flag route, the experiment, then + * the base binding. CLI sits above the capability clamp, so `--sequence + * orchestrator` still reaches the orchestrator's hard error in dev builds. + */ +function pickSequence( + ctx: SwitchboardCtx, +): [Required>['sequence'], Sequence] { + // The orchestrator owns the whole run lifecycle and cannot nest. + if (ctx.composed) return ['composed', Sequence.linear]; + if (!IS_PRODUCTION_BUILD && ctx.cliSequence) return ['cli', ctx.cliSequence]; + const { harness } = resolveHarness(ctx); + if (!harnessRunsTasks(harness)) { + if (ctx.orchestratorFlagOn) { + logToFile( + `[switchboard] wizard-orchestrator ignored: ${harness} has no runTask, clamping to linear`, + ); + } + return ['runtask-clamp', Sequence.linear]; + } + if (ctx.flagRoute?.sequence) return ['payload', ctx.flagRoute.sequence]; + if (ctx.flagSequence) return ['flag', ctx.flagSequence]; + return ['binding', (ctx.baseBinding ?? DEFAULT_AGENT_BINDING).sequence]; +} From bc7101a41c502303864ffa0e0bc8deeddb66ae8f Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 17:06:36 -0400 Subject: [PATCH 57/61] refactor(runners): move the legacy program adapter into runners Move src/programs/run-agent-legacy.ts to src/lib/runners/run-program-agent.ts with its content unchanged, the path B3 gives it. Its relative imports now use the @programs alias, its importers point at the new path, and the path references in comments and the agent READMEs follow it. The adapter test keeps its name and path, src/programs/__tests__/run-agent-legacy.test.ts, as it does in B3. known-violations.json trades the adapter's old edges for its new ones: the adapter now sits on the CLI surface and reaches ten programs modules directly, and the posthog-integration program imports it back. B3 removes those edges. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- scripts/tui-host.no-jest.ts | 2 +- .../architecture/known-violations.json | 16 +++++++---- src/__tests__/cli.test.ts | 2 +- src/__tests__/provision-cli.test.ts | 2 +- src/agent/README.md | 2 +- src/agent/agent-runner.ts | 2 +- src/agent/index.ts | 2 +- src/agent/progress.ts | 2 +- src/agent/runner/README.md | 2 +- src/agent/runner/index.ts | 2 +- src/agent/runner/shared/types.ts | 2 +- .../runners/__tests__/mint-recovery.test.ts | 4 +-- src/lib/runners/run-non-interactive.ts | 2 +- .../runners/run-program-agent.ts} | 27 ++++++++++++------- src/lib/runners/run-wizard.ts | 2 +- .../__tests__/run-agent-legacy.test.ts | 2 +- src/programs/posthog-integration/index.ts | 2 +- src/programs/program-run.ts | 2 +- 18 files changed, 46 insertions(+), 31 deletions(-) rename src/{programs/run-agent-legacy.ts => lib/runners/run-program-agent.ts} (95%) diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index c34f718eb..016cc9a28 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -27,7 +27,7 @@ import type { Harness, Sequence } from '@shared/constants'; import { buildSession } from '@lib/wizard-session'; import { initLocalDev } from '@shared/local-dev'; import { configureGatewayFromCIEnvironment } from '@agent/gateway-session'; -import { runProgramAgent } from '@programs/run-agent-legacy'; +import { runProgramAgent } from '@lib/runners/run-program-agent'; import { TaskStreamPush, createFileDestination } from '@programs/task-stream/index'; import { getAuditChecks } from '@programs/audit/types'; import { authenticate } from '@programs/authenticate'; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index e1bf710f5..070f1eb60 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -20,13 +20,21 @@ "src/env.ts -> src/lib/headless-mode.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/audit/types.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/detect-map.ts", - "src/lib/runners/run-non-interactive.ts -> src/programs/run-agent-legacy.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/task-stream/index.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/task-stream/task-stream-push.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/audit/ledger-watcher.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/authenticate.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/binding-telemetry.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/binding.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/commandments.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/experiments/index.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/posthog-integration/detect.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/program-run.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/program-step.ts", + "src/lib/runners/run-program-agent.ts -> src/programs/registry.ts", "src/lib/runners/run-wizard.ts -> src/programs/audit/types.ts", "src/lib/runners/run-wizard.ts -> src/programs/authenticate.ts", "src/lib/runners/run-wizard.ts -> src/programs/posthog-integration/detect.ts", - "src/lib/runners/run-wizard.ts -> src/programs/run-agent-legacy.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/destinations/file.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/destinations/posthog.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/index.ts", @@ -78,6 +86,7 @@ "src/programs/migration/index.ts -> src/ui/tui/decks/migration/index.tsx", "src/programs/migration/steps.ts -> src/lib/wizard-session.ts", "src/programs/posthog-integration/detect.ts -> src/lib/wizard-session.ts", + "src/programs/posthog-integration/index.ts -> src/lib/runners/run-program-agent.ts", "src/programs/posthog-integration/index.ts -> src/lib/wizard-session.ts", "src/programs/posthog-integration/index.ts -> src/steps/index.ts", "src/programs/posthog-integration/index.ts -> src/ui/index.ts", @@ -93,9 +102,6 @@ "src/programs/revenue-analytics/detect.ts -> src/lib/wizard-session.ts", "src/programs/revenue-analytics/index.ts -> src/ui/tui/decks/revenue-analytics/index.tsx", "src/programs/revenue-analytics/steps.ts -> src/lib/wizard-session.ts", - "src/programs/run-agent-legacy.ts -> src/lib/wizard-session.ts", - "src/programs/run-agent-legacy.ts -> src/ui/agent-progress.ts", - "src/programs/run-agent-legacy.ts -> src/ui/index.ts", "src/programs/self-driving/detect-agentic.ts -> src/lib/wizard-session.ts", "src/programs/self-driving/detect.ts -> src/lib/wizard-session.ts", "src/programs/self-driving/index.ts -> src/lib/wizard-session.ts", diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index c740d3fc8..953e7cdea 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -113,7 +113,7 @@ vi.mock('@utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../programs/run-agent-legacy', () => ({ +vi.mock('../lib/runners/run-program-agent', () => ({ runProgramAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 0224d25fa..b901fb59f 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -64,7 +64,7 @@ vi.mock('@utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../programs/run-agent-legacy', () => ({ +vi.mock('../lib/runners/run-program-agent', () => ({ runProgramAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/agent/README.md b/src/agent/README.md index 90352d0cf..3290ddcdf 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -85,7 +85,7 @@ store and no registry. Programs call the agent to do the work a skill describes. The TUI and the headless runner observe the run through `onProgress` and answer it through -`interaction`; today `src/programs/run-agent-legacy.ts` does both on top of +`interaction`; today `src/lib/runners/run-program-agent.ts` does both on top of the session. Without `onProgress` the run completes and its snapshot still comes back in the diff --git a/src/agent/agent-runner.ts b/src/agent/agent-runner.ts index 06a907fab..1fa1e4615 100644 --- a/src/agent/agent-runner.ts +++ b/src/agent/agent-runner.ts @@ -2,7 +2,7 @@ * Re-export shim. The runner has been split into agent/runner/. * Import from there directly; this shim keeps existing importers working. * The session-driven `runProgramAgent(programConfig, session)` lives in - * `src/programs/run-agent-legacy.ts`. + * `src/lib/runners/run-program-agent.ts`. */ export { diff --git a/src/agent/index.ts b/src/agent/index.ts index 2878fa23c..10eed6d50 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -34,7 +34,7 @@ export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; /** * Leaves in B2. Programs own credentials and the legacy adapter dies. * initializeAgent, executeAgent and buildRunTags are the pre-runAgent surface - * that detection/agentic.ts and run-agent-legacy.ts still call; they go + * that detection/agentic.ts and run-program-agent.ts still call; they go * through runAgent or leave with detection, and AgentErrorType, which * classifies executeAgent's failures, goes with them. * configureGatewayFromCIEnvironment is CI inference auth the headless provider diff --git a/src/agent/progress.ts b/src/agent/progress.ts index 10b38acfb..12507bd28 100644 --- a/src/agent/progress.ts +++ b/src/agent/progress.ts @@ -4,7 +4,7 @@ * `runAgent` reports through one optional callback and asks through one * optional set of capabilities. Neither reaches into a UI singleton, a store, * or a session: every payload is copied data, every question is awaited on an - * injected answerer. The legacy adapter in `src/programs/run-agent-legacy.ts` + * injected answerer. The legacy adapter in `src/lib/runners/run-program-agent.ts` * maps these back onto `WizardUI` one call per event, so the terminal output of * every existing runner is unchanged. */ diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index afd87744d..cb7349fcf 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -45,7 +45,7 @@ resolved execution data and an invocation snapshot (`shared/types.ts`), reports through `onProgress` and asks through `interaction` (`../progress.ts`), and returns every ending as a result. It never renders, reads a session or exits. The gates, OAuth, flags and binding lookup that used to run here live in -`src/programs/run-agent-legacy.ts`, which also maps progress back onto +`src/lib/runners/run-program-agent.ts`, which also maps progress back onto `getUI()` for today's runners. **Prepare** (`shared/bootstrap.ts`) is the on-ramp inside the agent: logging diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index 0544bffb3..0b5bf1f0f 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -19,7 +19,7 @@ * agent did not decide (a refused mint, an SDK crash) comes back as * `outcome: RunOutcome.Crashed` with the original error attached, so a caller can keep * handling it the way it always did. The legacy adapter in - * `src/programs/run-agent-legacy.ts` rebuilds today's session-driven + * `src/lib/runners/run-program-agent.ts` rebuilds today's session-driven * behavior on top of this call for every existing caller. */ diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index 9f2efd792..8d7c4c7c7 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -5,7 +5,7 @@ * invocation snapshot, reports through `options.onProgress`, asks through * `options.interaction`, and returns a `RunResult`. Nothing here names a UI, * a store, a session or a program registry: the caller resolves those and - * hands over plain data. `src/programs/run-agent-legacy.ts` is the caller + * hands over plain data. `src/lib/runners/run-program-agent.ts` is the caller * that rebuilds today's session-driven behavior on top of this contract. */ diff --git a/src/lib/runners/__tests__/mint-recovery.test.ts b/src/lib/runners/__tests__/mint-recovery.test.ts index ada5cf6c2..25d35c7a2 100644 --- a/src/lib/runners/__tests__/mint-recovery.test.ts +++ b/src/lib/runners/__tests__/mint-recovery.test.ts @@ -1,6 +1,6 @@ import { vi, it, expect, afterEach } from 'vitest'; import { runWizard } from '../run-wizard'; -import { runProgramAgent } from '@programs/run-agent-legacy'; +import { runProgramAgent } from '../run-program-agent'; import { startTUI } from '@ui/tui/start-tui'; import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; @@ -10,7 +10,7 @@ import { ScreenId } from '@ui/tui/router'; import { HostResolution } from '@shared/host-resolution'; import { analytics } from '@utils/analytics'; -vi.mock('@programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn() })); +vi.mock('../run-program-agent', () => ({ runProgramAgent: vi.fn() })); vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); vi.mock('@shared/local-dev', async (original) => ({ ...(await original()), diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 308532649..7ddb16154 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -337,7 +337,7 @@ export function runNonInteractive( } } - const { runProgramAgent } = await import('@programs/run-agent-legacy'); + const { runProgramAgent } = await import('./run-program-agent'); await runProgramAgent(config, session); await settleStream(RunPhase.Completed); } catch (error) { diff --git a/src/programs/run-agent-legacy.ts b/src/lib/runners/run-program-agent.ts similarity index 95% rename from src/programs/run-agent-legacy.ts rename to src/lib/runners/run-program-agent.ts index 8e2174986..92f2b102d 100644 --- a/src/programs/run-agent-legacy.ts +++ b/src/lib/runners/run-program-agent.ts @@ -26,11 +26,17 @@ import { TASK_OUTCOMES_KEY, } from '@agent'; import type { RunConfig, RunInput } from '@agent/types'; -import { resolveProgramBinding, type ProgramSwitchboardCtx } from './binding'; -import { getProgramCommandments } from './commandments'; -import { captureSwitchboardDecision } from './binding-telemetry'; -import { areSeededTasksEnabled, resolveStageOverrides } from './experiments'; -import type { ProgramRun } from './program-run'; +import { + resolveProgramBinding, + type ProgramSwitchboardCtx, +} from '@programs/binding'; +import { getProgramCommandments } from '@programs/commandments'; +import { captureSwitchboardDecision } from '@programs/binding-telemetry'; +import { + areSeededTasksEnabled, + resolveStageOverrides, +} from '@programs/experiments'; +import type { ProgramRun } from '@programs/program-run'; import { backupAndFixClaudeSettings, checkAllSettingsConflicts, @@ -54,10 +60,13 @@ import { type Integration, } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@programs/registry'; -import { postAuthGateSteps, type ProgramConfig } from './program-step'; -import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; -import { maybeStampAiSdkDetected } from './posthog-integration/detect'; -import { startAuditLedgerWatcher } from './audit/ledger-watcher'; +import { postAuthGateSteps, type ProgramConfig } from '@programs/program-step'; +import { + authenticate, + refreshAccessTokenIfNeeded, +} from '@programs/authenticate'; +import { maybeStampAiSdkDetected } from '@programs/posthog-integration/detect'; +import { startAuditLedgerWatcher } from '@programs/audit/ledger-watcher'; /** * Resolve a ProgramConfig's agent run definition and execute the pipeline. diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index 2ef2ff9cf..ab12530d6 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,6 +1,6 @@ import { VERSION } from '@shared/version'; import { logToFile, getLogFilePath } from '@utils/debug'; -import { runProgramAgent } from '@programs/run-agent-legacy'; +import { runProgramAgent } from './run-program-agent'; import { authenticate } from '@programs/authenticate'; import { getProgramConfig } from '@programs'; import { getAuditChecks } from '@programs/audit/types'; diff --git a/src/programs/__tests__/run-agent-legacy.test.ts b/src/programs/__tests__/run-agent-legacy.test.ts index efa72b0ea..e99cb4ab4 100644 --- a/src/programs/__tests__/run-agent-legacy.test.ts +++ b/src/programs/__tests__/run-agent-legacy.test.ts @@ -1,7 +1,7 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; import { runWizard } from '@lib/runners/run-wizard'; import { authenticate } from '@programs/authenticate'; -import { runProgramAgent } from '../run-agent-legacy'; +import { runProgramAgent } from '@lib/runners/run-program-agent'; import { runAgent, RunOutcome, type RunResult } from '@agent/runner'; import { Harness, Sequence } from '@shared/constants'; import { buildSession, OutroKind } from '@lib/wizard-session'; diff --git a/src/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts index e1d91c70f..3f72530c3 100644 --- a/src/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig, ProgramStep } from '@programs/program-step'; -import { runProgramAgent } from '@programs/run-agent-legacy'; +import { runProgramAgent } from '@lib/runners/run-program-agent'; import type { ProgramRun } from '@programs/program-run'; import { AgentSignals, shouldDisableAsk, WIZARD_TOOL_NAMES } from '@agent'; import type { WizardSession } from '@lib/wizard-session'; diff --git a/src/programs/program-run.ts b/src/programs/program-run.ts index a3d9a15be..b11a394eb 100644 --- a/src/programs/program-run.ts +++ b/src/programs/program-run.ts @@ -1,7 +1,7 @@ /** * A program's run definition: the agent's `AgentRunDefinition` plus the * completion hooks that read the session. The agent never calls these — - * `run-agent-legacy.ts` binds them to the run's credentials and hands the + * `src/lib/runners/run-program-agent.ts` binds them to the run's credentials and hands the * agent `RunConfig.hooks`. */ From b0afe6b051e5eb80f47093ab355bdeefd65c8630 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 18:24:10 -0400 Subject: [PATCH 58/61] revert(b1): restore the original B1 tree, dropping the folded-in B2a moves and the adapter rename Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .claude/skills/adding-skill-program/SKILL.md | 4 +- .claude/skills/wizard-development/SKILL.md | 6 +- .../references/ARCHITECTURE.md | 18 +- AGENTS.md | 2 +- docs/benchmarking.md | 2 +- scripts/a3-fault-probe.no-jest.ts | 5 + scripts/tui-host.no-jest.ts | 2 +- .../architecture/known-violations.json | 21 ++- src/__tests__/cli.test.ts | 2 +- src/__tests__/provision-cli.test.ts | 2 +- src/agent/README.md | 22 ++- src/agent/__tests__/commandments.test.ts | 10 +- .../__tests__/harness-capabilities.test.ts | 16 -- .../__tests__/run-agent-standalone.test.ts | 40 +++++ .../__tests__/run-tags.test.ts | 2 +- .../__tests__/variant-gating.test.ts | 2 +- src/agent/agent-interface.ts | 35 +++- src/agent/agent-runner.ts | 2 +- src/agent/default-binding.ts | 10 -- src/agent/gateway-session.ts | 104 +++++++++++- src/agent/index.ts | 22 ++- src/agent/progress.ts | 2 +- src/agent/runner/README.md | 19 +-- .../runner}/__tests__/switchboard.test.ts | 12 +- .../__tests__/pending-question.test.ts | 1 + src/agent/runner/harness/anthropic/index.ts | 1 - src/agent/runner/harness/pi/index.ts | 2 +- src/agent/runner/harness/pi/task.ts | 2 +- src/agent/runner/index.ts | 3 +- src/agent/runner/sequence/README.md | 5 +- .../orchestrator/orchestrator-runner.ts | 30 +++- src/agent/runner/shared/bootstrap.ts | 24 ++- src/agent/runner/shared/types.ts | 21 +-- src/agent/runner/switchboard/commandments.ts | 29 +++- .../flags}/__tests__/binding-cases.ts | 9 +- .../flags}/__tests__/flags.test.ts | 34 ++-- .../runner/switchboard/flags}/index.ts | 0 .../runner/switchboard/flags}/orchestrator.ts | 0 .../runner/switchboard/flags}/schemes.ts | 2 +- .../runner/switchboard/flags}/self-driving.ts | 0 src/agent/runner/switchboard/harness.ts | 87 +++++++++- src/agent/runner/switchboard/index.ts | 154 ++++++++++++++--- .../runner/switchboard/resolve-harness.ts | 90 ---------- src/agent/runner/switchboard/sequence.ts | 97 ++++++++++- src/agent/tools/tool-names.ts | 24 --- src/agent/tools/tools.ts | 23 ++- src/agent/types.ts | 6 +- src/agent/wizard-ask-bridge.ts | 8 +- src/{shared => lib}/file-watcher.ts | 0 .../runners/__tests__/mint-recovery.test.ts | 4 +- src/lib/runners/run-non-interactive.ts | 2 +- src/lib/runners/run-wizard.ts | 2 +- src/lib/wizard-session.ts | 30 +++- .../__tests__/run-agent-legacy.test.ts | 2 +- src/programs/agent-skill/index.ts | 45 ++++- src/programs/agent-skill/run-definition.ts | 43 ----- src/programs/ai-observability/index.ts | 38 ++++- src/programs/ai-observability/run.ts | 39 ----- src/programs/audit/ledger-watcher.ts | 2 +- src/programs/binding-telemetry.ts | 50 ------ src/programs/binding.ts | 160 ------------------ src/programs/commandments.ts | 18 -- src/programs/mcp-analytics/index.ts | 62 ++++++- src/programs/mcp-analytics/run.ts | 62 ------- src/programs/metrics/index.ts | 45 ++++- src/programs/metrics/run.ts | 44 ----- src/programs/migration/index.ts | 44 ++++- src/programs/migration/run.ts | 40 ----- src/programs/posthog-integration/index.ts | 2 +- src/programs/program-run.ts | 2 +- src/programs/replay-vision/index.ts | 47 ++++- src/programs/replay-vision/run.ts | 47 ----- src/programs/revenue-analytics/abort-cases.ts | 25 --- src/programs/revenue-analytics/detect.ts | 25 ++- src/programs/revenue-analytics/index.ts | 14 +- src/programs/revenue-analytics/run.ts | 14 -- .../run-agent-legacy.ts} | 92 +++++++--- .../task-stream/event-plan-watcher.ts | 2 +- src/programs/types.ts | 1 - .../web-analytics-doctor/abort-cases.ts | 34 ---- src/programs/web-analytics-doctor/detect.ts | 34 +++- src/programs/web-analytics-doctor/index.ts | 27 ++- src/programs/web-analytics-doctor/run.ts | 27 --- src/shared/ask-policy.ts | 31 ---- src/shared/gateway-auth.ts | 99 ----------- src/shared/run-tags.ts | 26 --- src/shared/scan-consent.ts | 26 --- src/shared/self-driving-pricing.ts | 30 ---- src/shared/utils/cleanup-registry.ts | 22 --- src/shared/utils/oauth-token.ts | 84 --------- src/shared/utils/oauth.ts | 87 ++++++++-- src/shared/utils/wizard-abort.ts | 25 ++- src/ui/tui/decks/self-driving/pricing.ts | 37 +++- src/ui/tui/hooks/file-watcher.ts | 12 +- 94 files changed, 1273 insertions(+), 1345 deletions(-) delete mode 100644 src/agent/__tests__/harness-capabilities.test.ts rename src/{shared => agent}/__tests__/run-tags.test.ts (97%) rename src/{programs => agent}/__tests__/variant-gating.test.ts (89%) delete mode 100644 src/agent/default-binding.ts rename src/{programs => agent/runner}/__tests__/switchboard.test.ts (97%) rename src/{programs/experiments => agent/runner/switchboard/flags}/__tests__/binding-cases.ts (91%) rename src/{programs/experiments => agent/runner/switchboard/flags}/__tests__/flags.test.ts (93%) rename src/{programs/experiments => agent/runner/switchboard/flags}/index.ts (100%) rename src/{programs/experiments => agent/runner/switchboard/flags}/orchestrator.ts (100%) rename src/{programs/experiments => agent/runner/switchboard/flags}/schemes.ts (99%) rename src/{programs/experiments => agent/runner/switchboard/flags}/self-driving.ts (100%) delete mode 100644 src/agent/runner/switchboard/resolve-harness.ts delete mode 100644 src/agent/tools/tool-names.ts rename src/{shared => lib}/file-watcher.ts (100%) delete mode 100644 src/programs/agent-skill/run-definition.ts delete mode 100644 src/programs/ai-observability/run.ts delete mode 100644 src/programs/binding-telemetry.ts delete mode 100644 src/programs/binding.ts delete mode 100644 src/programs/commandments.ts delete mode 100644 src/programs/mcp-analytics/run.ts delete mode 100644 src/programs/metrics/run.ts delete mode 100644 src/programs/migration/run.ts delete mode 100644 src/programs/replay-vision/run.ts delete mode 100644 src/programs/revenue-analytics/abort-cases.ts delete mode 100644 src/programs/revenue-analytics/run.ts rename src/{lib/runners/run-program-agent.ts => programs/run-agent-legacy.ts} (85%) delete mode 100644 src/programs/web-analytics-doctor/abort-cases.ts delete mode 100644 src/programs/web-analytics-doctor/run.ts delete mode 100644 src/shared/ask-policy.ts delete mode 100644 src/shared/gateway-auth.ts delete mode 100644 src/shared/run-tags.ts delete mode 100644 src/shared/scan-consent.ts delete mode 100644 src/shared/self-driving-pricing.ts delete mode 100644 src/shared/utils/cleanup-registry.ts delete mode 100644 src/shared/utils/oauth-token.ts diff --git a/.claude/skills/adding-skill-program/SKILL.md b/.claude/skills/adding-skill-program/SKILL.md index 28e4ed022..3aedd8778 100644 --- a/.claude/skills/adding-skill-program/SKILL.md +++ b/.claude/skills/adding-skill-program/SKILL.md @@ -62,8 +62,8 @@ changing execution behavior. 3. Register the config in [PROGRAM_REGISTRY](../../../src/programs/program-registry.ts) and add its Pi/orchestrator entry to - [PROGRAM_BINDINGS](../../../src/programs/binding.ts). - [Existing binding checks](../../../src/programs/__tests__/switchboard.test.ts) + [PROGRAM_BINDINGS](../../../src/agent/runner/switchboard/index.ts). + [Existing binding checks](../../../src/agent/runner/__tests__/switchboard.test.ts) enforce coverage; `ProgramId` currently widens to `string`. 4. For a standalone native command, create a command module with [nativeCommandFactory](../../../src/commands/factories/native-command-factory.ts) diff --git a/.claude/skills/wizard-development/SKILL.md b/.claude/skills/wizard-development/SKILL.md index c9819acd4..5fee5c7a5 100644 --- a/.claude/skills/wizard-development/SKILL.md +++ b/.claude/skills/wizard-development/SKILL.md @@ -22,7 +22,7 @@ infrastructure should consume those boundaries. | Framework detection, context, env conventions | [FrameworkConfig](../../../src/programs/framework-config.ts) and [framework configs](../../../src/programs/frameworks/) | | Integration instructions and orchestrator flows/tasks | [context-mill](https://github.com/PostHog/context-mill) | | Programs, steps, prerequisites and outcomes | [programs](../../../src/programs/) | -| Sequence, harness, model and effort selection | [program bindings](../../../src/programs/binding.ts) and [agent clamps](../../../src/agent/runner/switchboard/) | +| Sequence, harness, model and effort selection | [switchboard](../../../src/agent/runner/switchboard/) | | Local tool permissions and scanner adapters | [agent-interface](../../../src/agent/agent-interface.ts), [YARA hooks](../../../src/agent/yara-hooks.ts), [Pi security](../../../src/agent/runner/harness/pi/security.ts) | | Scanner rules | [warlock](https://github.com/PostHog/warlock) | | Token admission and budgets | [PostHog mint endpoint](https://github.com/PostHog/posthog/blob/master/posthog/llm/wizard_gateway_token.py) and [ai-gateway](https://github.com/PostHog/ai-gateway) | @@ -43,8 +43,8 @@ infrastructure should consume those boundaries. new Anthropic models. Existing routing has not all migrated: -[DEFAULT_AGENT_BINDING](../../../src/agent/default-binding.ts) selects Pi + linear -for standalone runs; programs apply their own binding and flag overrides. Set new +[DEFAULT_BINDING](../../../src/agent/runner/switchboard/index.ts) still +selects Anthropic + linear, with per-program and flag overrides. Set new bindings explicitly. Migrating an existing program requires checking its flow, tasks, and lifecycle hooks; changing the default constant alone is insufficient. Both harnesses implement `run` and `runTask`. diff --git a/.claude/skills/wizard-development/references/ARCHITECTURE.md b/.claude/skills/wizard-development/references/ARCHITECTURE.md index 72a0228b4..570d1e5e1 100644 --- a/.claude/skills/wizard-development/references/ARCHITECTURE.md +++ b/.claude/skills/wizard-development/references/ARCHITECTURE.md @@ -60,11 +60,10 @@ example. Native command modules still need registration in ## Switchboard contract -`resolveProgramBinding(ctx)` in [programs](../../../../src/programs/binding.ts) -is the routing seam. It receives the program, flag snapshot/payloads, -composition state and development overrides, returning a resolved binding and -stamping a trace of the selected precedence rungs. The agent receives this -binding and any pre-resolved task-role routes as data. +`resolveBinding(ctx, role?)` is the routing seam. Read its exported types rather +than copying their fields into another document. It receives the program, flag +snapshot/payloads, composition state, and development overrides, returning the +binding and stamping a trace of the selected precedence rungs. - Harness/model: development CLI override, declared flag route, per-program binding, default. @@ -73,8 +72,7 @@ binding and any pre-resolved task-role routes as data. [Harness](../../../../src/agent/runner/switchboard/harness.ts) and [sequence](../../../../src/agent/runner/switchboard/sequence.ts) contain the -generic precedence and clamp chains over caller-supplied policy. Published -builds omit CLI overrides. `RUN_SURFACE` can disable +exact chains. Published builds omit CLI overrides. `RUN_SURFACE` can disable harness experiments; static bindings and harness capabilities also affect resolution. Composed sub-runs remain linear even when a CLI override requests orchestration. @@ -86,11 +84,11 @@ admitted by the minted token and gateway. Local routing cannot bypass that external policy; see the [model admission checklist](../SKILL.md#execution-policy-and-model-admission). -Program flags belong in -[experiments](../../../../src/programs/experiments/). +Flags belong in +[switchboard/flags](../../../../src/agent/runner/switchboard/flags/). Experiments declare their program scope; malformed payloads yield no experiment route. Reuse the -[switchboard tests](../../../../src/programs/__tests__/switchboard.test.ts) +[switchboard tests](../../../../src/agent/runner/__tests__/switchboard.test.ts) and experiment tests to check full bindings and isolation of unrelated programs. Do not add a second flag-reading path inside a harness or sequence. diff --git a/AGENTS.md b/AGENTS.md index 17463d33e..773faf9cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,7 +76,7 @@ Agent SDK is a supported legacy fallback, deprecated as the default; retain it for major Pi vulnerabilities or gaps in support for new Anthropic models. This is the contribution policy, not a claim that every existing binding has -migrated. The default, `DEFAULT_AGENT_BINDING`, is Pi + linear. Set new bindings +migrated: `DEFAULT_BINDING` is still Anthropic + linear. Set new bindings explicitly and check sequence-specific hooks before migrating existing flows. See [execution policy and model admission](.claude/skills/wizard-development/SKILL.md#execution-policy-and-model-admission) diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 8d4a15bd9..e5fadf4f7 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -102,7 +102,7 @@ Everything below ships in this repo (`wizard/`) and its workbench orchestrator on pi, per-task models from context-mill frontmatter; off → the linear anthropic default). Per-stage variations ride `wizard-orchestrator-override` payloads (`{stage: {model?, effort?}}`, - variant keys in `wizard/src/programs/experiments/schemes.ts`). + variant keys in `wizard/src/agent/runner/switchboard/flags/schemes.ts`). The baseline is `{"wizard-orchestrator":"false"}` — never an empty override, or live remote flags leak into the baseline. diff --git a/scripts/a3-fault-probe.no-jest.ts b/scripts/a3-fault-probe.no-jest.ts index 58ed9d8bd..91b83e5ac 100644 --- a/scripts/a3-fault-probe.no-jest.ts +++ b/scripts/a3-fault-probe.no-jest.ts @@ -66,6 +66,11 @@ const config: RunConfig = { harness: harness as Harness, model: DEFAULT_AGENT_MODEL, }, + switchboard: { + program: 'fault-probe', + flags: {}, + cliHarness: harness as Harness, + }, skillsBaseUrl: 'http://127.0.0.1:1', wizardFlags: {}, wizardFlagPayloads: {}, diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index 016cc9a28..c34f718eb 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -27,7 +27,7 @@ import type { Harness, Sequence } from '@shared/constants'; import { buildSession } from '@lib/wizard-session'; import { initLocalDev } from '@shared/local-dev'; import { configureGatewayFromCIEnvironment } from '@agent/gateway-session'; -import { runProgramAgent } from '@lib/runners/run-program-agent'; +import { runProgramAgent } from '@programs/run-agent-legacy'; import { TaskStreamPush, createFileDestination } from '@programs/task-stream/index'; import { getAuditChecks } from '@programs/audit/types'; import { authenticate } from '@programs/authenticate'; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 070f1eb60..0d110b9d3 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -1,5 +1,8 @@ { "violations": [ + "src/agent/runner/switchboard/flags/index.ts -> src/programs/types.ts", + "src/agent/runner/switchboard/flags/schemes.ts -> src/programs/types.ts", + "src/agent/runner/switchboard/index.ts -> src/programs/types.ts", "src/commands/ai-observability.ts -> src/programs/ai-observability/index.ts", "src/commands/audit.ts -> src/programs/audit/index.ts", "src/commands/basic-integration/ci-install.ts -> src/programs/posthog-integration/index.ts", @@ -20,21 +23,13 @@ "src/env.ts -> src/lib/headless-mode.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/audit/types.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/detect-map.ts", + "src/lib/runners/run-non-interactive.ts -> src/programs/run-agent-legacy.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/task-stream/index.ts", "src/lib/runners/run-non-interactive.ts -> src/programs/task-stream/task-stream-push.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/audit/ledger-watcher.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/authenticate.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/binding-telemetry.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/binding.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/commandments.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/experiments/index.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/posthog-integration/detect.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/program-run.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/program-step.ts", - "src/lib/runners/run-program-agent.ts -> src/programs/registry.ts", "src/lib/runners/run-wizard.ts -> src/programs/audit/types.ts", "src/lib/runners/run-wizard.ts -> src/programs/authenticate.ts", "src/lib/runners/run-wizard.ts -> src/programs/posthog-integration/detect.ts", + "src/lib/runners/run-wizard.ts -> src/programs/run-agent-legacy.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/destinations/file.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/destinations/posthog.ts", "src/lib/runners/run-wizard.ts -> src/programs/task-stream/index.ts", @@ -47,6 +42,7 @@ "src/programs/ai-opt-in-gate.ts -> src/lib/wizard-session.ts", "src/programs/audit/index.ts -> src/lib/headless-mode.ts", "src/programs/audit/index.ts -> src/lib/wizard-session.ts", + "src/programs/audit/ledger-watcher.ts -> src/lib/file-watcher.ts", "src/programs/audit/ledger-watcher.ts -> src/ui/index.ts", "src/programs/audit/types.ts -> src/lib/wizard-session.ts", "src/programs/authenticate.ts -> src/lib/wizard-session.ts", @@ -86,7 +82,6 @@ "src/programs/migration/index.ts -> src/ui/tui/decks/migration/index.tsx", "src/programs/migration/steps.ts -> src/lib/wizard-session.ts", "src/programs/posthog-integration/detect.ts -> src/lib/wizard-session.ts", - "src/programs/posthog-integration/index.ts -> src/lib/runners/run-program-agent.ts", "src/programs/posthog-integration/index.ts -> src/lib/wizard-session.ts", "src/programs/posthog-integration/index.ts -> src/steps/index.ts", "src/programs/posthog-integration/index.ts -> src/ui/index.ts", @@ -102,6 +97,9 @@ "src/programs/revenue-analytics/detect.ts -> src/lib/wizard-session.ts", "src/programs/revenue-analytics/index.ts -> src/ui/tui/decks/revenue-analytics/index.tsx", "src/programs/revenue-analytics/steps.ts -> src/lib/wizard-session.ts", + "src/programs/run-agent-legacy.ts -> src/lib/wizard-session.ts", + "src/programs/run-agent-legacy.ts -> src/ui/agent-progress.ts", + "src/programs/run-agent-legacy.ts -> src/ui/index.ts", "src/programs/self-driving/detect-agentic.ts -> src/lib/wizard-session.ts", "src/programs/self-driving/detect.ts -> src/lib/wizard-session.ts", "src/programs/self-driving/index.ts -> src/lib/wizard-session.ts", @@ -113,6 +111,7 @@ "src/programs/shared/posthog-cli-preinstall.ts -> src/steps/install-cli-steering/index.ts", "src/programs/shared/posthog-cli-preinstall.ts -> src/ui/index.ts", "src/programs/task-stream/destinations/posthog.ts -> src/lib/wizard-session.ts", + "src/programs/task-stream/event-plan-watcher.ts -> src/lib/file-watcher.ts", "src/programs/task-stream/event-plan-watcher.ts -> src/ui/tui/store.ts", "src/programs/task-stream/task-stream-push.ts -> src/lib/wizard-session.ts", "src/programs/task-stream/task-stream-push.ts -> src/ui/tui/store.ts", diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index 953e7cdea..c740d3fc8 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -113,7 +113,7 @@ vi.mock('@utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../lib/runners/run-program-agent', () => ({ +vi.mock('../programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index b901fb59f..0224d25fa 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -64,7 +64,7 @@ vi.mock('@utils/wizard-abort', async (importOriginal) => ({ ...(await importOriginal()), wizardAbort: vi.fn(), })); -vi.mock('../lib/runners/run-program-agent', () => ({ +vi.mock('../programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn().mockResolvedValue(undefined), })); diff --git a/src/agent/README.md b/src/agent/README.md index 3290ddcdf..4116171cd 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -20,11 +20,10 @@ runAgent(config: RunConfig, input: RunInput, options?: { }): Promise ``` -- `RunConfig`: the opaque program id, its `AgentRunDefinition` (prompt, skill, - tools, copy), the resolved `binding` (sequence, harness, model and task-role - routes), supplied program commandments and stage policy, the skills origin, - flag snapshot, trace tags, tool allow and deny lists, seed tasks and bound - completion `hooks`. +- `RunConfig`: the program id, its `AgentRunDefinition` (prompt, skill, tools, + copy), the resolved `binding` (sequence, harness, model), the switchboard + inputs, the skills origin, flag snapshot, trace tags, tool allow and deny + lists, seed tasks and bound completion `hooks`. - `RunInput`: install directory, resolved credentials, project and user payloads, skill id, detected integration, `flags` (`ci`, `signup`, `debug`, `e2eAsk`, `localMcp`, `captureAio`, `benchmark`, `yaraReport`) and the host @@ -55,12 +54,11 @@ runAgent(config: RunConfig, input: RunInput, options?: { `setup wizard finished` event. The host sends it from the outcome: `Success` is `success`, `Aborted` is `cancelled`, `Failed` and `Crashed` are `error`. -Other runtime exports: `DEFAULT_AGENT_BINDING` for standalone callers, -`resolveHarness` and `harnessRunsTasks`, which programs resolve a binding with, -`shouldDisableAsk`, `initializeAgent`, `executeAgent`, `buildRunTags`, -`AgentSignals`, `configureGatewayFromCIEnvironment`, `downloadSkill`, -`WIZARD_TOOL_NAMES`, `LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and -`runMcpPromptViaSdk`, which loads the streaming module on first call. +Other runtime exports: `resolveBinding`, `shouldDisableAsk`, `initializeAgent`, +`executeAgent`, `buildRunTags`, `AgentSignals`, +`configureGatewayFromCIEnvironment`, `downloadSkill`, `WIZARD_TOOL_NAMES`, +`LONGER_ASK_TIMEOUT_MS`, `flushScanReport`, and `runMcpPromptViaSdk`, which +loads the streaming module on first call. Minimal invocation: @@ -85,7 +83,7 @@ store and no registry. Programs call the agent to do the work a skill describes. The TUI and the headless runner observe the run through `onProgress` and answer it through -`interaction`; today `src/lib/runners/run-program-agent.ts` does both on top of +`interaction`; today `src/programs/run-agent-legacy.ts` does both on top of the session. Without `onProgress` the run completes and its snapshot still comes back in the diff --git a/src/agent/__tests__/commandments.test.ts b/src/agent/__tests__/commandments.test.ts index d4a9722d5..2a694c0f2 100644 --- a/src/agent/__tests__/commandments.test.ts +++ b/src/agent/__tests__/commandments.test.ts @@ -1,6 +1,5 @@ import { WIZARD_COMMANDMENTS } from '@agent/commandments'; import { assembleCommandments } from '@agent/runner/switchboard/commandments'; -import { getProgramCommandments } from '@programs/commandments'; import { Harness, Sequence } from '@shared/constants'; const global = WIZARD_COMMANDMENTS.join('\n'); @@ -11,13 +10,7 @@ const prompt = ( harness: Harness, sequence: Sequence, program = 'posthog-integration', -) => - assembleCommandments({ - programCommandments: getProgramCommandments(program), - sequence, - harness, - caps: CAPS, - }); +) => assembleCommandments({ program, sequence, harness, caps: CAPS }); const COMBOS = [ ['anthropic', Harness.anthropic, Sequence.linear], @@ -155,6 +148,7 @@ describe('commandments by axis', () => { describe('runtime caps gate the pi runtime notes', () => { const withCaps = (caps: { bash: boolean; posthogMcp: boolean }) => assembleCommandments({ + program: 'warehouse-source', sequence: Sequence.linear, harness: Harness.pi, caps, diff --git a/src/agent/__tests__/harness-capabilities.test.ts b/src/agent/__tests__/harness-capabilities.test.ts deleted file mode 100644 index d491f90d0..000000000 --- a/src/agent/__tests__/harness-capabilities.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Harness } from '@shared/constants'; -import { HARNESS_OPTIONS } from '@agent/runner/switchboard/harness'; -import { HARNESS_RUNS_TASKS } from '@agent/runner/switchboard/resolve-harness'; - -describe('harness capabilities', () => { - it.each(Object.values(Harness))( - 'records whether the %s backend implements runTask', - (harness) => { - const backend = HARNESS_OPTIONS[harness]; - expect(backend).toBeDefined(); - expect(HARNESS_RUNS_TASKS[harness]).toBe( - typeof backend?.runTask === 'function', - ); - }, - ); -}); diff --git a/src/agent/__tests__/run-agent-standalone.test.ts b/src/agent/__tests__/run-agent-standalone.test.ts index 24c3f39a8..df73af8fe 100644 --- a/src/agent/__tests__/run-agent-standalone.test.ts +++ b/src/agent/__tests__/run-agent-standalone.test.ts @@ -171,6 +171,10 @@ vi.mock('@agent/runner/switchboard/harness', () => { harnessState.selected.push(name); return { ...fake, name }; }, + resolveHarness: (ctx: { cliHarness?: Harness }) => ({ + harness: ctx.cliHarness ?? Harness.pi, + model: DEFAULT_AGENT_MODEL, + }), }; }); @@ -233,6 +237,7 @@ const config = (over: Partial = {}): RunConfig => ({ }, composed: false, binding: { sequence: Sequence.linear, harness: Harness.pi, model: 'm' }, + switchboard: { program: 'test-program', flags: {} }, skillsBaseUrl: 'https://skills.test', wizardFlags: {}, wizardFlagPayloads: {}, @@ -311,6 +316,11 @@ describe('runAgent standalone', () => { const running = runAgent( config({ binding: { harness, sequence, model: DEFAULT_AGENT_MODEL }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: harness, + }, }), input(), { interaction: { ask }, onProgress: (event) => events.push(event) }, @@ -377,6 +387,11 @@ describe('runAgent standalone', () => { const result = await runAgent( config({ binding: { harness, sequence, model: DEFAULT_AGENT_MODEL }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: harness, + }, }), input(), ); @@ -412,6 +427,11 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.anthropic, + }, }), input(), ); @@ -434,6 +454,11 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.anthropic, + }, }), input(), ); @@ -457,6 +482,11 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.pi, + }, }), input(), ); @@ -502,6 +532,11 @@ describe('runAgent standalone', () => { sequence: Sequence.orchestrator, model: DEFAULT_AGENT_MODEL, }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.pi, + }, }), input(), { @@ -887,6 +922,11 @@ describe('runAgent standalone', () => { sequence, model: DEFAULT_AGENT_MODEL, }, + switchboard: { + program: 'test-program', + flags: {}, + cliHarness: Harness.pi, + }, }), input(), { diff --git a/src/shared/__tests__/run-tags.test.ts b/src/agent/__tests__/run-tags.test.ts similarity index 97% rename from src/shared/__tests__/run-tags.test.ts rename to src/agent/__tests__/run-tags.test.ts index 9a01156bb..8211b96e8 100644 --- a/src/shared/__tests__/run-tags.test.ts +++ b/src/agent/__tests__/run-tags.test.ts @@ -1,4 +1,4 @@ -import { buildRunTags } from '@shared/run-tags'; +import { buildRunTags } from '@agent/agent-interface'; import { CallType } from '@shared/constants'; describe('buildRunTags', () => { diff --git a/src/programs/__tests__/variant-gating.test.ts b/src/agent/__tests__/variant-gating.test.ts similarity index 89% rename from src/programs/__tests__/variant-gating.test.ts rename to src/agent/__tests__/variant-gating.test.ts index b0d6ed00e..7efd2ca49 100644 --- a/src/programs/__tests__/variant-gating.test.ts +++ b/src/agent/__tests__/variant-gating.test.ts @@ -1,4 +1,4 @@ -import { isOrchestratorEnabled } from '@programs/experiments'; +import { isOrchestratorEnabled } from '@agent/runner/switchboard'; describe('isOrchestratorEnabled', () => { it('is true only when the wizard-orchestrator flag is true', () => { diff --git a/src/agent/agent-interface.ts b/src/agent/agent-interface.ts index ff13db4d2..005315236 100644 --- a/src/agent/agent-interface.ts +++ b/src/agent/agent-interface.ts @@ -211,8 +211,6 @@ export type AgentConfig = { * another program's budget, so neither should depend on an optional string bag. */ programId: string; - /** Program-owned guidance supplied as data, never looked up here. */ - programCommandments?: readonly string[]; /** Program identifier — selects the model for that program. */ integrationLabel?: string; /** @@ -357,8 +355,8 @@ type AgentRunConfig = { * bearer. */ refreshGatewayAuth?: () => Promise; - /** Program-owned guidance supplied as data. */ - programCommandments?: readonly string[]; + /** Program id, for the program-axis commandments. */ + program?: string; /** Resolved sequence, for the sequence-axis commandments. */ sequence: Sequence; /** Where the run reports. A no-op when the caller passed none. */ @@ -367,7 +365,30 @@ type AgentRunConfig = { const NO_PROGRESS: ProgressEmitter = () => undefined; -export { buildRunTags } from '@shared/run-tags'; +/** + * Global identifiers attached to every LLM gateway trace for a run. They ride on + * each `$ai_generation` the gateway emits (in the `X-PostHog-Properties` blob + * `buildAgentEnv` builds), so traces are filterable by program, framework, run, + * and build type for cost attribution and dashboards. `skill_id` is omitted when + * the run has none. + */ +export function buildRunTags(args: { + programId: string; + integration: string; + runId: string; + build: string; + skillId?: string; +}): Record { + return { + program_id: args.programId, + integration: args.integration, + run_id: args.runId, + build: args.build, + // Triage and detection spread these tags and override this one. + call_type: CallType.agent, + ...(args.skillId ? { skill_id: args.skillId } : {}), + }; +} /** * Whether Warlock/YARA scanning is disabled for this run. Off by default: @@ -648,7 +669,7 @@ export async function initializeAgent( triageProvider, gatewayAuth: auth, refreshGatewayAuth: currentGatewayAuth, - programCommandments: config.programCommandments, + program: config.integrationLabel, // A queue context is present only on a task run; that is the sequence. sequence: config.orchestrator ? Sequence.orchestrator : Sequence.linear, emit, @@ -1148,7 +1169,7 @@ export async function runAgent( // we keep default Claude Code behaviors. An orchestrator context is // present only on a task run — that is what picks the sequence. append: assembleCommandments({ - programCommandments: agentConfig.programCommandments, + program: agentConfig.program, sequence: agentConfig.sequence, harness: Harness.anthropic, }), diff --git a/src/agent/agent-runner.ts b/src/agent/agent-runner.ts index 1fa1e4615..06a907fab 100644 --- a/src/agent/agent-runner.ts +++ b/src/agent/agent-runner.ts @@ -2,7 +2,7 @@ * Re-export shim. The runner has been split into agent/runner/. * Import from there directly; this shim keeps existing importers working. * The session-driven `runProgramAgent(programConfig, session)` lives in - * `src/lib/runners/run-program-agent.ts`. + * `src/programs/run-agent-legacy.ts`. */ export { diff --git a/src/agent/default-binding.ts b/src/agent/default-binding.ts deleted file mode 100644 index 7bad35666..000000000 --- a/src/agent/default-binding.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { GPT5_6_SOL_MODEL, Harness, Sequence } from '@shared/constants'; -import type { ResolvedBinding } from './runner/shared/types'; - -/** Standalone callers can supply this resolved route without a program registry. */ -export const DEFAULT_AGENT_BINDING: ResolvedBinding = { - sequence: Sequence.linear, - harness: Harness.pi, - model: GPT5_6_SOL_MODEL, - thinkingLevel: 'medium', -}; diff --git a/src/agent/gateway-session.ts b/src/agent/gateway-session.ts index c1bb23d59..e8a1ef1b2 100644 --- a/src/agent/gateway-session.ts +++ b/src/agent/gateway-session.ts @@ -15,14 +15,21 @@ import { checkLlmGatewayHealth } from '@shared/health-checks/endpoints'; import { ServiceHealthStatus } from '@shared/health-checks/types'; import { IS_PRODUCTION_BUILD, runtimeEnv } from '@env'; import type { CloudRegion } from '@utils/types'; -import { isTrustedGatewayUrl, type GatewayAuth } from '@shared/gateway-auth'; -export { - buildWizardPropertiesBlob, - isPastRefresh, - isTrustedGatewayUrl, - type GatewayAuth, -} from '@shared/gateway-auth'; +export interface GatewayAuth { + /** Base URL for model calls (no `/v1`; transports append their route). */ + gatewayUrl: string; + /** Gateway bearer, minted normally or supplied directly by CI. */ + token: string; + /** Team verified by the mint, or explicitly supplied for CI attribution. */ + teamId?: number; + /** + * Instant past which a 401 on this bearer is age rather than a bad + * credential: the cache re-mints past it, and a session still holding the + * old bearer may re-mint once. Before it the mint has to be trusted. + */ + refreshAtMs: number; +} interface CachedAuth { key: string; @@ -184,6 +191,54 @@ export function resetGatewaySession(): void { ciAuth = null; } +/** Whether a 401 on this bearer may be age (past its refresh instant) rather than a bad credential. */ +export function isPastRefresh(auth: GatewayAuth, now = Date.now()): boolean { + return now >= auth.refreshAtMs; +} + +/** + * Whether a server-supplied origin may receive a bearer and prompt content: + * https (loopback excepted), and either a current cloud gateway or the host the run + * authenticated against. + */ +export function isTrustedGatewayUrl(value: string, apiHost: string): boolean { + let url: URL; + try { + url = new URL(value); + } catch { + return false; + } + // Consumers append routes to this value, so anything beyond an origin + // (path, query, fragment, userinfo) would build a malformed endpoint. + if ( + url.pathname !== '/' || + url.search || + url.hash || + url.username || + url.password + ) { + return false; + } + const localhost = + url.hostname === 'localhost' || + url.hostname === '127.0.0.1' || + url.hostname === 'host.docker.internal'; + // Loopback is the dev gateway, and is the one case allowed over http. + if (localhost) return true; + if (url.protocol !== 'https:') return false; + if (url.hostname.endsWith('.posthog.com')) { + return ( + url.origin === 'https://ai-gateway.us.posthog.com' || + url.origin === 'https://ai-gateway.eu.posthog.com' + ); + } + try { + return url.hostname === new URL(apiHost).hostname; + } catch { + return false; + } +} + interface MintedToken { token: string; expiresAt: string; @@ -395,3 +450,38 @@ async function mintGatewayToken( ); } } + +/** + * The v2 run-metadata carrier: one JSON blob for the `X-PostHog-Properties` + * header. Plain keys only, since the gateway strips `$`-prefixed keys as reserved, + * so feature-flag variants land as `wizard_flag_` instead of the legacy + * `$feature/` (dashboards keying on `$feature/wizard-*` read the new key + * post-cutover). + */ +export function buildWizardPropertiesBlob( + wizardMetadata: Record, + wizardFlags: Record, + teamId?: number, +): string { + // The gateway pins `$ai_product` to `wizard:`, and rejects a legacy + // product override on a scoped token, so the unprefixed key every cost and + // error consumer reads is only present if this blob declares it. + const props: Record = { ai_product: 'wizard' }; + if (teamId !== undefined) props.team_id = teamId; + for (const [key, value] of Object.entries(wizardMetadata)) { + props[stripPropertyPrefix(key)] = value; + } + for (const [flagKey, variant] of Object.entries(wizardFlags)) { + if (!flagKey.toLowerCase().startsWith('wizard')) continue; + props[`wizard_flag_${flagKey.toLowerCase()}`] = variant; + } + return JSON.stringify(props); +} + +const LEGACY_PROPERTY_PREFIX = 'X-POSTHOG-PROPERTY-'; + +function stripPropertyPrefix(key: string): string { + return key.toUpperCase().startsWith(LEGACY_PROPERTY_PREFIX) + ? key.slice(LEGACY_PROPERTY_PREFIX.length).toLowerCase() + : key; +} diff --git a/src/agent/index.ts b/src/agent/index.ts index 10eed6d50..605eeb0c8 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -10,31 +10,29 @@ /** * Stays. The agent's contract: the one way to run it, the marker strings - * program prompts embed, the tool ids programs put in allowedTools and - * disallowedTools, and what programs resolve a binding with: the default - * binding, the harness axis and each harness's task capability. + * program prompts embed, and the tool ids programs put in allowedTools and + * disallowedTools. */ export type * from './types'; export { runAgent, RunOutcome } from './runner'; export { AgentSignals } from './agent-interface'; export { WIZARD_TOOL_NAMES } from './tools'; -export { DEFAULT_AGENT_BINDING } from './default-binding'; -export { - harnessRunsTasks, - resolveHarness, -} from './runner/switchboard/resolve-harness'; /** - * Leaves in B2. shouldDisableAsk is a flags policy programs decide and pass - * in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own as askTimeoutMs. + * Leaves in B2 (B1 deferred the bindings). Bindings and program data move to + * programs: resolveBinding + * is keyed by PROGRAM_BINDINGS and the agent keeps only "run from an + * already-resolved binding"; shouldDisableAsk is a flags policy programs + * decide and pass in; LONGER_ASK_TIMEOUT_MS is a tuning number programs own + * as askTimeoutMs. */ -export { shouldDisableAsk } from './runner'; +export { resolveBinding, shouldDisableAsk } from './runner'; export { LONGER_ASK_TIMEOUT_MS } from './wizard-ask-bridge'; /** * Leaves in B2. Programs own credentials and the legacy adapter dies. * initializeAgent, executeAgent and buildRunTags are the pre-runAgent surface - * that detection/agentic.ts and run-program-agent.ts still call; they go + * that detection/agentic.ts and run-agent-legacy.ts still call; they go * through runAgent or leave with detection, and AgentErrorType, which * classifies executeAgent's failures, goes with them. * configureGatewayFromCIEnvironment is CI inference auth the headless provider diff --git a/src/agent/progress.ts b/src/agent/progress.ts index 12507bd28..10b38acfb 100644 --- a/src/agent/progress.ts +++ b/src/agent/progress.ts @@ -4,7 +4,7 @@ * `runAgent` reports through one optional callback and asks through one * optional set of capabilities. Neither reaches into a UI singleton, a store, * or a session: every payload is copied data, every question is awaited on an - * injected answerer. The legacy adapter in `src/lib/runners/run-program-agent.ts` + * injected answerer. The legacy adapter in `src/programs/run-agent-legacy.ts` * maps these back onto `WizardUI` one call per event, so the terminal output of * every existing runner is unchanged. */ diff --git a/src/agent/runner/README.md b/src/agent/runner/README.md index cb7349fcf..480c1280b 100644 --- a/src/agent/runner/README.md +++ b/src/agent/runner/README.md @@ -23,7 +23,7 @@ retained for very simple tasks and legacy support. The Anthropic Agent SDK is a supported legacy fallback, deprecated as the default, retained for major Pi vulnerabilities or gaps in support for new Anthropic models. -`DEFAULT_AGENT_BINDING`, the standalone default, is Pi + linear; explicit program bindings +Existing `DEFAULT_BINDING` remains Anthropic + linear; explicit program bindings and flags determine actual behavior. Both harnesses implement `run` and `runTask`. Composed sub-runs are clamped to linear, and linear-only post-run/outro hooks do not automatically transfer to an orchestrated flow. @@ -45,22 +45,21 @@ resolved execution data and an invocation snapshot (`shared/types.ts`), reports through `onProgress` and asks through `interaction` (`../progress.ts`), and returns every ending as a result. It never renders, reads a session or exits. The gates, OAuth, flags and binding lookup that used to run here live in -`src/lib/runners/run-program-agent.ts`, which also maps progress back onto +`src/programs/run-agent-legacy.ts`, which also maps progress back onto `getUI()` for today's runners. **Prepare** (`shared/bootstrap.ts`) is the on-ramp inside the agent: logging targets, the gateway mint and the scan-triage classifier. Whether the run turns out to be linear or orchestrator, anthropic or pi, the setup is the same. -**The switchboard** (`switchboard/`) holds the sequence and harness registries -and the harness axis. Programs resolve the binding (`resolveProgramBinding`): -the sequence precedence lives there, and the harness and model come from this -layer's `resolveHarness` (CLI > flag > program config > -default). `harnessRunsTasks` tells programs which harnesses the orchestrator can -drive. +**The switchboard** (`switchboard/`) is the router. Given a program id + the +fetched flags + any CLI overrides, it returns a `ProgramBinding` — which query +shape (sequence), which agent SDK (harness), which model. Two independent +middleware chains, one per axis, apply precedence rules (CLI > flag > program +config > default). This is the only layer that makes routing decisions. -**Sequences** (`sequence/`) are LLM query shapes. Once the binding has picked -one, that sequence takes over the run and owns _how the LLM's work is +**Sequences** (`sequence/`) are LLM query shapes. Once the switchboard has +picked one, that sequence takes over the run and owns _how the LLM's work is shaped_. See `sequence/README.md`. - **linear** — one long conversation with the model, start to finish. diff --git a/src/programs/__tests__/switchboard.test.ts b/src/agent/runner/__tests__/switchboard.test.ts similarity index 97% rename from src/programs/__tests__/switchboard.test.ts rename to src/agent/runner/__tests__/switchboard.test.ts index d6efea02d..0a16a2048 100644 --- a/src/programs/__tests__/switchboard.test.ts +++ b/src/agent/runner/__tests__/switchboard.test.ts @@ -2,7 +2,7 @@ * Switchboard machinery tests: binding registry lockstep, precedence chains, * trace stamping, model capabilities, and structural clamps. Per-experiment * flag behavior and cross-program isolation live in one file per experiment - * under `experiments/__tests__/`. + * under `switchboard/flags/__tests__/`. * * Every resolution test is a BindingCase: (SwitchboardCtx in) → (full * four-axis resolveBinding out), optionally pinning the trace. @@ -24,10 +24,10 @@ import { } from '@shared/constants'; import { PROGRAM_BINDINGS, - resolveProgramBinding as resolveBinding, -} from '../binding'; -import type { ProgramSwitchboardCtx as SwitchboardCtx } from '@programs/types'; -import { DEFAULT_AGENT_BINDING as DEFAULT_BINDING } from '@agent'; + DEFAULT_BINDING, + resolveBinding, + type SwitchboardCtx, +} from '@agent/runner/switchboard'; import { modelCapabilities, MINT_ALLOWED_EFFORTS, @@ -36,7 +36,7 @@ import { TRIAGE_MODELS, VALID_MODELS, } from '@agent/runner/switchboard/models'; -import { runBindingCases } from '@programs/experiments/__tests__/binding-cases'; +import { runBindingCases } from '@agent/runner/switchboard/flags/__tests__/binding-cases'; const PROGRAM_IDS = PROGRAM_REGISTRY.map((c) => c.id); const DEFAULT_RESOLVED = { diff --git a/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts b/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts index bd7580a5f..86ff59c3f 100644 --- a/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts +++ b/src/agent/runner/harness/anthropic/__tests__/pending-question.test.ts @@ -44,6 +44,7 @@ async function initializeHarness( sequence: Sequence.linear, model: 'test', }, + switchboard: { program: 'test', flags: {} }, skillsBaseUrl: 'https://skills.test', wizardFlags: {}, wizardFlagPayloads: {}, diff --git a/src/agent/runner/harness/anthropic/index.ts b/src/agent/runner/harness/anthropic/index.ts index 4d622f8ad..8a75169bf 100644 --- a/src/agent/runner/harness/anthropic/index.ts +++ b/src/agent/runner/harness/anthropic/index.ts @@ -136,7 +136,6 @@ export const anthropicBackend: AgentHarness = { detectPackageManager: detectNodePackageManagers, skillsBaseUrl: boot.skillsBaseUrl, programId: boot.programId, - programCommandments: config.programCommandments, wizardFlags: boot.wizardFlags, wizardMetadata: boot.wizardMetadata, integrationLabel: config.programId, diff --git a/src/agent/runner/harness/pi/index.ts b/src/agent/runner/harness/pi/index.ts index 17e9b6660..90ea3ac54 100644 --- a/src/agent/runner/harness/pi/index.ts +++ b/src/agent/runner/harness/pi/index.ts @@ -394,7 +394,7 @@ export const piBackend: AgentHarness = { agentDir: getAgentDir(), systemPrompt: assembleCommandments({ - programCommandments: runConfig.programCommandments, + program: runConfig.programId, sequence: Sequence.linear, harness: Harness.pi, caps: { bash: true, posthogMcp }, diff --git a/src/agent/runner/harness/pi/task.ts b/src/agent/runner/harness/pi/task.ts index 6caf8e62d..290950f40 100644 --- a/src/agent/runner/harness/pi/task.ts +++ b/src/agent/runner/harness/pi/task.ts @@ -340,7 +340,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { cwd: input.installDir, agentDir: getAgentDir(), systemPrompt: assembleCommandments({ - programCommandments: config.programCommandments, + program: config.programId, sequence: Sequence.orchestrator, harness: Harness.pi, caps: { bash: codingTools.has('bash'), posthogMcp }, diff --git a/src/agent/runner/index.ts b/src/agent/runner/index.ts index 0b5bf1f0f..91d35dedc 100644 --- a/src/agent/runner/index.ts +++ b/src/agent/runner/index.ts @@ -19,7 +19,7 @@ * agent did not decide (a refused mint, an SDK crash) comes back as * `outcome: RunOutcome.Crashed` with the original error attached, so a caller can keep * handling it the way it always did. The legacy adapter in - * `src/lib/runners/run-program-agent.ts` rebuilds today's session-driven + * `src/programs/run-agent-legacy.ts` rebuilds today's session-driven * behavior on top of this call for every existing caller. */ @@ -62,6 +62,7 @@ export type { ProgressEmitter, } from '@agent/progress'; export { shouldDisableAsk } from './shared/bootstrap'; +export { resolveBinding } from './switchboard'; export type { ProgramBinding, SwitchboardCtx } from './switchboard'; export { TASK_OUTCOMES_KEY } from './sequence/orchestrator/queue'; export type { TaskOutcome } from './sequence/orchestrator/queue'; diff --git a/src/agent/runner/sequence/README.md b/src/agent/runner/sequence/README.md index ce795d52b..ac4436f9a 100644 --- a/src/agent/runner/sequence/README.md +++ b/src/agent/runner/sequence/README.md @@ -49,7 +49,6 @@ frontmatter contract rather than copying an old manifest example: task types after planning. It is not a universal automatic graph built from frontmatter. -Per-role routes in the resolved binding can adjust a task's harness, model or -effort. Programs resolve `PROGRAM_BINDINGS[id].contextMillOverride` before the -agent starts. New task types usually belong in context-mill; new native +Per-role `PROGRAM_BINDINGS[id].contextMillOverride` can adjust a task's harness, +model or effort. New task types usually belong in context-mill; new native behavior still requires the appropriate Wizard configuration or implementation. diff --git a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts index 850453cb3..a5d42f8ab 100644 --- a/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts +++ b/src/agent/runner/sequence/orchestrator/orchestrator-runner.ts @@ -43,8 +43,10 @@ import type { import { createEmitSpinner } from '../../shared/progress-collector'; import { createAskBridge } from '../../shared/ask'; import { + areSeededTasksEnabled, getHarness, - resolveRoleHarness, + resolveHarness, + resolveStageOverrides, type HarnessPick, } from '../../switchboard'; import { isValidModel, requireKnownModel } from '../../switchboard/models'; @@ -606,6 +608,11 @@ async function executeOrchestrator( const { run } = config; const programId = config.programId; + // Switchboard context — reused for every per-role harness resolution below. + // The caller resolved the run-level binding from it; per-task roles overlay + // `binding.contextMillOverride[role]` on the same inputs. + const switchboardCtx = { ...config.switchboard, trace: undefined }; + // The WHAT (agent prompts) is served from context-mill. Fetch the registry // once up front: its types drive enqueue validation, and resolving a task to // its run config is then synchronous, with no mid-drain network latency. @@ -613,7 +620,11 @@ async function executeOrchestrator( const registry = await loadAgentRegistry(boot.skillsBaseUrl, flow, { exclude: effectiveExcludedTaskTypes(config, boot.wizardFlags), // Baked into the prompts at load, so enqueue, dispatch, and telemetry all read one effective spec. - overrides: config.stageOverrides, + overrides: resolveStageOverrides( + programId, + boot.wizardFlags, + boot.wizardFlagPayloads, + ), }); if (signal?.aborted) return cancelledRun(); const seedPrompt = registry.seed; @@ -627,7 +638,7 @@ async function executeOrchestrator( const taskModels = Object.fromEntries( ['seed', ...registry.types].map((type) => { const prompt = type === 'seed' ? seedPrompt : registry.get(type); - const pick = resolveRoleHarness(config.binding, type); + const pick = resolveHarness(switchboardCtx, type); const specModel = prompt && promptModelFor(prompt, pick.harness).model; return [type, isValidModel(specModel) ? specModel : pick.model]; }), @@ -652,7 +663,7 @@ async function executeOrchestrator( const store = new QueueStore(input.installDir, runId, { onTransition: (event, task) => { - const pick = resolveRoleHarness(config.binding, task.type); + const pick = resolveHarness(switchboardCtx, task.type); // Mirror dispatch's allow-list fallback so attribution names the model that runs. const specModel = taskModelSpec(registry, task, pick.harness).model; const base = { @@ -869,7 +880,7 @@ async function executeOrchestrator( // depend on them, and no prompt has to remember they are there. // Kill switch: off (or unset), the wizard queues nothing itself and the run // is byte-identical to a project with no detected sources. - const seedEntries = config.seededTasksEnabled + const seedEntries = areSeededTasksEnabled(boot.wizardFlags) ? config.seedTasks?.() ?? [] : []; const seededTypes: string[] = []; @@ -986,7 +997,7 @@ async function executeOrchestrator( // Prompt-frontmatter model wins over the switchboard pick (§3.6 of the // switchboard plan) — the switchboard's model is the fallback when the // prompt is silent. - const seedPick = resolveRoleHarness(config.binding, 'seed'); + const seedPick = resolveHarness(switchboardCtx, 'seed'); const seedHarness = requireTaskHarness(seedPick); const seedModel = promptModelFor(seedPrompt, seedPick.harness); const seedResult = await seedHarness.runTask({ @@ -1196,9 +1207,10 @@ async function executeOrchestrator( // panel shows progress); errors still surface — the harness stops the // spinner with its own error text. // - // Per-task role = task.type. Programs resolved any role override before - // invocation; prompt-frontmatter model still wins (§3.6). - const taskPick = resolveRoleHarness(config.binding, task.type); + // Per-task role = task.type — the switchboard consults + // PROGRAM_BINDINGS[id].contextMillOverride?.[task.type] for wizard-side + // per-agent overrides. Prompt-frontmatter model still wins (§3.6). + const taskPick = resolveHarness(switchboardCtx, task.type); const taskHarness = requireTaskHarness(taskPick); const taskModel = taskModelSpec(registry, task, taskPick.harness); let taskResult: AgentResult; diff --git a/src/agent/runner/shared/bootstrap.ts b/src/agent/runner/shared/bootstrap.ts index 4bb164dda..1ed76bae0 100644 --- a/src/agent/runner/shared/bootstrap.ts +++ b/src/agent/runner/shared/bootstrap.ts @@ -15,11 +15,31 @@ import { CallType, IS_DEV } from '@shared/constants'; import { VERSION } from '@shared/version'; import { mcpUrlFor } from '@shared/host-resolution'; import type { WizardRunOptions } from '@utils/types'; -import type { BootstrapResult, RunConfig, RunInput } from './types'; +import type { BootstrapResult, RunConfig, RunFlags, RunInput } from './types'; // ── Helpers ────────────────────────────────────────────────────────── -export { isAskDisabled as shouldDisableAsk } from '@shared/ask-policy'; +/** + * 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. + * + * `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( + flags: Pick, +): boolean { + return (flags.ci || flags.signup) && !flags.e2eAsk; +} /** The option bag the agent interface and the middleware read. */ export function runOptions(input: RunInput): WizardRunOptions { diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index 8d7c4c7c7..b82f7af7b 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -5,7 +5,7 @@ * invocation snapshot, reports through `options.onProgress`, asks through * `options.interaction`, and returns a `RunResult`. Nothing here names a UI, * a store, a session or a program registry: the caller resolves those and - * hands over plain data. `src/lib/runners/run-program-agent.ts` is the caller + * hands over plain data. `src/programs/run-agent-legacy.ts` is the caller * that rebuilds today's session-driven behavior on top of this contract. */ @@ -21,6 +21,7 @@ import type { ErrorCode } from '@shared/errors'; import type { LLMProvider } from '@posthog/warlock'; import type { AgentInteraction, ProgressEmitter } from '@agent/progress'; import type { EffortLevel } from '../switchboard/models'; +import type { SwitchboardCtx } from '../switchboard'; export type { PromptContext, Credentials }; @@ -138,11 +139,6 @@ export interface ResolvedBinding { model: string; /** Reasoning-effort override. Absent → the model's table default. */ thinkingLevel?: EffortLevel; - /** Role-specific routes resolved by the caller before the agent starts. */ - roleBindings?: Record< - string, - { harness: Harness; model: string; thinkingLevel?: EffortLevel } - >; } /** @@ -151,7 +147,7 @@ export interface ResolvedBinding { * treats every label as opaque. */ export interface RunConfig { - /** Opaque program label for gateway spend pin and analytics. */ + /** Program id: gateway spend pin, analytics label, commandments axis. */ programId: string; /** The run definition. A program's session-taking hooks are the caller's, see `hooks`. */ run: AgentRunDefinition; @@ -159,12 +155,11 @@ export interface RunConfig { composed: boolean; /** Run-level sequence, harness and model. */ binding: ResolvedBinding; - /** Program text selected by the caller; the agent only assembles it. */ - programCommandments?: readonly string[]; - /** Validated stage policy selected by the caller; absent keeps flow frontmatter. */ - stageOverrides?: Record; - /** The caller's resolved seeded-task experiment. */ - seededTasksEnabled?: boolean; + /** + * The inputs the run-level binding was resolved from. The orchestrator + * re-resolves the harness per task role from these; nothing else reads them. + */ + switchboard: SwitchboardCtx; /** Primary skills origin (context-mill dev or GitHub Releases). */ skillsBaseUrl: string; /** Feature flag key → variant, evaluated before the run. */ diff --git a/src/agent/runner/switchboard/commandments.ts b/src/agent/runner/switchboard/commandments.ts index a170b271a..5f709b1fb 100644 --- a/src/agent/runner/switchboard/commandments.ts +++ b/src/agent/runner/switchboard/commandments.ts @@ -1,8 +1,11 @@ /** * System-prompt commandments, keyed by the axes the switchboard resolves. * - * Programs select their guidance before the run; the agent assembles that - * supplied text with global, sequence, model and harness guidance. + * A run is a resolved (program, sequence, harness, model). Guidance belongs to + * whichever axis makes it true, declared here beside the tables that resolve + * them, and assembled once by `assembleCommandments`. A rule that is true for + * every run stays in `@lib/agent/commandments`; anything narrower lives here so + * the call sites never re-derive it. * * Leaf module by design — it imports the axis enums and the per-axis text, never * a runner or a harness backend, so the harnesses can call the assembler without @@ -37,6 +40,20 @@ const SEQUENCE_COMMANDMENTS: Record = { [Sequence.orchestrator]: [], }; +// ── Program axis ──────────────────────────────────────────────────────── + +const SELF_DRIVING = [ + 'ALWAYS surface a custom-scout proposal in step 6b: bring the user your one or two strongest candidate scouts even when the built-in troop looks sufficient. The proposal ask leads with a "None — keep the built-in troop" option, so declining costs the user one keystroke — but a proposal you silently skip is coverage they never got to see or judge. Where the skill says to skip the ask when the gap analysis finds no candidate, do NOT skip: pick your best candidates anyway and let the user decide.', + + 'Rank candidates at the discriminator level, not the category level. "Covered" only means an enabled scout would actually FIRE for that failure mode: a conversion-rate watcher does not catch entry volume collapsing; a Stripe-transaction watcher does not catch a lead form going silent. A surface whose failure mode has no firing condition among the enabled scouts is your strongest candidate.', + + 'Be honest in the option descriptions: if a candidate overlaps something an enabled scout partially watches, say so in its description rather than dropping the candidate. The user chooses with full information; you do not gatekeep on their behalf.', +]; + +const PROGRAM_COMMANDMENTS: Record = { + 'self-driving': SELF_DRIVING, +}; + // ── Harness axis ──────────────────────────────────────────────────────── /** @@ -58,8 +75,8 @@ const MODEL_COMMANDMENTS: Record = {}; // ── Assembly ──────────────────────────────────────────────────────────── export interface CommandmentAxes { - /** Selected by programs; the agent only assembles supplied text. */ - programCommandments?: readonly string[]; + /** Program id, as resolved into `PROGRAM_BINDINGS`. */ + program?: string; sequence: Sequence; harness: Harness; /** Gateway model id. */ @@ -70,14 +87,14 @@ export interface CommandmentAxes { /** Every commandment this run's axes call for, broad to narrow. */ export function assembleCommandments(axes: CommandmentAxes): string { - const { programCommandments, sequence, harness, model, caps } = axes; + const { program, sequence, harness, model, caps } = axes; const harnessNotes = HARNESS_NOTES[harness]?.( sequence, caps ?? { bash: true, posthogMcp: true }, ); return [ ...WIZARD_COMMANDMENTS, - ...(programCommandments ?? []), + ...(program ? PROGRAM_COMMANDMENTS[program] ?? [] : []), ...SEQUENCE_COMMANDMENTS[sequence], ...(model ? MODEL_COMMANDMENTS[model] ?? [] : []), // Blank line first: the notes open their own `## This runtime` section. diff --git a/src/programs/experiments/__tests__/binding-cases.ts b/src/agent/runner/switchboard/flags/__tests__/binding-cases.ts similarity index 91% rename from src/programs/experiments/__tests__/binding-cases.ts rename to src/agent/runner/switchboard/flags/__tests__/binding-cases.ts index da7a31439..341a5d9a8 100644 --- a/src/programs/experiments/__tests__/binding-cases.ts +++ b/src/agent/runner/switchboard/flags/__tests__/binding-cases.ts @@ -5,8 +5,11 @@ */ import { describe, it, expect } from 'vitest'; import { GPT5_6_SOL_MODEL, Harness, Sequence } from '@shared/constants'; -import { resolveProgramBinding as resolveBinding } from '../../binding'; -import type { ProgramSwitchboardCtx as SwitchboardCtx } from '@programs/types'; +import { + resolveBinding, + type SwitchboardCtx, + type SwitchboardTrace, +} from '@agent/runner/switchboard'; import type { EffortLevel } from '@agent/runner/switchboard/models'; /** The complete resolved binding — every axis stated, nothing implicit. */ @@ -24,7 +27,7 @@ export interface BindingCase { ctx: Omit; binding: ExpectedBinding; /** Also pin which precedence rung decided each axis. */ - trace?: SwitchboardCtx['trace']; + trace?: SwitchboardTrace; } export function runBindingCases( diff --git a/src/programs/experiments/__tests__/flags.test.ts b/src/agent/runner/switchboard/flags/__tests__/flags.test.ts similarity index 93% rename from src/programs/experiments/__tests__/flags.test.ts rename to src/agent/runner/switchboard/flags/__tests__/flags.test.ts index e34adf86c..8c04f0983 100644 --- a/src/programs/experiments/__tests__/flags.test.ts +++ b/src/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -15,14 +15,17 @@ import { WIZARD_ORCHESTRATOR_OVERRIDE_FLAG_KEY, WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, } from '@shared/constants'; -import { resolveProgramBinding as resolveBinding } from '../../binding'; -import type { ProgramSwitchboardCtx as SwitchboardCtx } from '@programs/types'; -import { areSeededTasksEnabled, resolveStageOverrides } from '..'; +import { + areSeededTasksEnabled, + resolveBinding, + resolveStageOverrides, + type SwitchboardCtx, +} from '@agent/runner/switchboard'; import { ORCHESTRATOR_SEQUENCE_ROUTE, ORCHESTRATOR_HARNESS_ROUTE, -} from '@programs/experiments/orchestrator'; -import { SELF_DRIVING_EXPERIMENT } from '@programs/experiments/self-driving'; +} from '@agent/runner/switchboard/flags/orchestrator'; +import { SELF_DRIVING_EXPERIMENT } from '@agent/runner/switchboard/flags/self-driving'; import { runBindingCases } from './binding-cases'; const envState = vi.hoisted(() => ({ @@ -329,17 +332,22 @@ describe('isolation — everything on at once', () => { }); }); -describe('seam scan — routing reads live only in experiments/', () => { - const programsDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +describe('seam scan — routing reads live only in flags/', () => { + const switchboardDir = join( + dirname(fileURLToPath(import.meta.url)), + '..', + '..', + ); + // orchestrator-runner consumes a flags/ resolver; it may pass the snapshot through, never index it. for (const file of [ - '../agent/runner/switchboard/harness.ts', - '../agent/runner/switchboard/sequence.ts', - '../agent/runner/switchboard/models.ts', - '../agent/runner/switchboard/index.ts', - '../agent/runner/sequence/orchestrator/orchestrator-runner.ts', + 'harness.ts', + 'sequence.ts', + 'models.ts', + 'index.ts', + '../sequence/orchestrator/orchestrator-runner.ts', ]) { it(`${file} contains no direct flag reads or flag-key imports`, () => { - const src = readFileSync(join(programsDir, file), 'utf8'); + const src = readFileSync(join(switchboardDir, file), 'utf8'); expect(src).not.toMatch(/ctx\.flags\[/); expect(src).not.toMatch(/flags\[['"`]/); expect(src).not.toMatch(/WIZARD_\w+_FLAG_KEY/); diff --git a/src/programs/experiments/index.ts b/src/agent/runner/switchboard/flags/index.ts similarity index 100% rename from src/programs/experiments/index.ts rename to src/agent/runner/switchboard/flags/index.ts diff --git a/src/programs/experiments/orchestrator.ts b/src/agent/runner/switchboard/flags/orchestrator.ts similarity index 100% rename from src/programs/experiments/orchestrator.ts rename to src/agent/runner/switchboard/flags/orchestrator.ts diff --git a/src/programs/experiments/schemes.ts b/src/agent/runner/switchboard/flags/schemes.ts similarity index 99% rename from src/programs/experiments/schemes.ts rename to src/agent/runner/switchboard/flags/schemes.ts index 1fd30ce34..99f9c4a97 100644 --- a/src/programs/experiments/schemes.ts +++ b/src/agent/runner/switchboard/flags/schemes.ts @@ -15,7 +15,7 @@ import { } from '@shared/constants'; import type { ProgramId } from '@programs/types'; import { logToFile } from '@utils/debug'; -import type { EffortLevel } from '@agent/types'; +import type { EffortLevel } from '../models'; // ── Shared vocabulary ───────────────────────────────────────────────────── diff --git a/src/programs/experiments/self-driving.ts b/src/agent/runner/switchboard/flags/self-driving.ts similarity index 100% rename from src/programs/experiments/self-driving.ts rename to src/agent/runner/switchboard/flags/self-driving.ts diff --git a/src/agent/runner/switchboard/harness.ts b/src/agent/runner/switchboard/harness.ts index b9078ac34..0f5673ed2 100644 --- a/src/agent/runner/switchboard/harness.ts +++ b/src/agent/runner/switchboard/harness.ts @@ -1,12 +1,22 @@ /** - * Harness axis: the registry. Mirrors `sequence.ts`; the resolver is - * `resolve-harness.ts`. + * Harness axis: registry, middleware, resolver. Mirrors `sequence.ts`. */ +import { IS_PRODUCTION_BUILD } from '@env'; import { Harness } from '@shared/constants'; +import { logToFile } from '@utils/debug'; import { anthropicBackend } from '../harness/anthropic'; import { piBackend } from '../harness/pi'; import type { AgentHarness } from '../harness/types'; +import { resolveFlagRoute } from './flags'; +import { + DEFAULT_BINDING, + PROGRAM_BINDINGS, + runChain, + type HarnessPick, + type Middleware, + type SwitchboardCtx, +} from '.'; export const HARNESS_OPTIONS: Partial> = { [Harness.anthropic]: anthropicBackend, @@ -20,3 +30,76 @@ export function getHarness(name: Harness): AgentHarness { } return harness; } + +/** + * PostHog-flag routing to pi (see `./flags`). No valid route — flag off, no + * config, or an invalid payload — keeps the non-flagged binding default. + */ +const flagRunnerOverride: Middleware = (ctx, next) => { + const pick = next(); + const route = resolveFlagRoute(ctx.program, ctx.flags, ctx.flagPayloads); + if (!route) return pick; + if (ctx.trace) { + ctx.trace.harness = 'flag'; + // Harness-only routes keep the binding's model — trace it truthfully so + // analytics never attributes the fallback model to the flag. + if (route.model) ctx.trace.model = 'flag'; + } + return { + harness: route.harness ?? Harness.pi, + model: route.model ?? pick.model, + thinkingLevel: route.thinkingLevel ?? pick.thinkingLevel, + }; +}; + +/** `--harness` override. Dev/test only — the option is gated out of published builds. */ +const cliHarnessOverride: Middleware = (ctx, next) => { + const pick = next(); + if (!ctx.cliHarness) return pick; + if (ctx.trace) ctx.trace.harness = 'cli'; + return { ...pick, harness: ctx.cliHarness }; +}; + +/** `--model` override. Dev/test only — the option is gated out of published builds. */ +const cliModelOverride: Middleware = (ctx, next) => { + const pick = next(); + if (!ctx.cliModel) return pick; + if (ctx.trace) ctx.trace.model = 'cli'; + return { ...pick, model: ctx.cliModel }; +}; + +// Order = precedence: CLI > flag > binding default. The prod spread collapses +// to [], dropping the CLI overrides from the chain. +const HARNESS_MIDDLEWARE: Middleware[] = [ + ...(IS_PRODUCTION_BUILD ? [] : [cliHarnessOverride, cliModelOverride]), + flagRunnerOverride, +]; + +/** + * Resolve the harness for a role. Linear callers omit `role`; orchestrator + * callers pass `'seed'` or `task.type`. `contextMillOverride[role]` overlays. + */ +export function resolveHarness( + ctx: SwitchboardCtx, + role = 'default', +): HarnessPick { + const pick = runChain(HARNESS_MIDDLEWARE, ctx, () => { + if (ctx.trace) + Object.assign(ctx.trace, { harness: 'binding', model: 'binding' }); + const binding = PROGRAM_BINDINGS[ctx.program] ?? DEFAULT_BINDING; + return { + harness: binding.harness, + model: binding.model, + thinkingLevel: binding.thinkingLevel, + ...binding.contextMillOverride?.[role], + }; + }); + logToFile( + `[switchboard] resolved: program=${ctx.program} harness=${pick.harness}` + + `${ctx.trace?.harness ? ` (${ctx.trace.harness})` : ''} model=${ + pick.model + }` + + `${ctx.trace?.model ? ` (${ctx.trace.model})` : ''}`, + ); + return pick; +} diff --git a/src/agent/runner/switchboard/index.ts b/src/agent/runner/switchboard/index.ts index 7b6203f81..b2d84add2 100644 --- a/src/agent/runner/switchboard/index.ts +++ b/src/agent/runner/switchboard/index.ts @@ -1,11 +1,20 @@ // Resolves routing; model additions also require mint allowlists and gateway prompt/transport support. -import { Harness, Sequence } from '@shared/constants'; +import { + DEFAULT_AGENT_MODEL, + GPT5_6_SOL_MODEL, + GPT5_6_TERRA_MODEL, + Harness, + Sequence, +} from '@shared/constants'; +import type { ProgramId } from '@programs/types'; +import { resolveHarness } from './harness'; import type { EffortLevel } from './models'; +import { resolveSequence } from './sequence'; // ── Shared machinery ──────────────────────────────────────────────────── -/** Which precedence rung decided each axis. Stamped by the resolvers as they decide. */ +/** Which precedence rung decided each axis. Stamped by middlewares as they assert. */ export interface SwitchboardTrace { harness?: 'cli' | 'flag' | 'binding'; model?: 'cli' | 'flag' | 'binding'; @@ -18,21 +27,14 @@ export interface SwitchboardTrace { | 'binding'; } -/** Everything a resolver may branch on. Built once per run. */ +/** Everything a resolver middleware may branch on. Built once per run. */ export interface SwitchboardCtx { - /** Opaque log label. Program lookup stays with the caller. */ - program?: string; - /** The caller's selected base binding, before route/CLI overlays. */ - baseBinding?: ProgramBinding; + program: ProgramId; /** Composed sub-run (a dependency inside a parent program). Structurally linear — no override can orchestrate it. */ composed?: boolean; - /** Already validated experiment route; no flag parsing happens in the agent. */ - flagRoute?: { - harness?: Harness; - model?: string; - thinkingLevel?: EffortLevel; - sequence?: Sequence; - }; + flags: Record; + /** Flag payloads from the same snapshot (payload-carrying flags, e.g. self-driving pi). */ + flagPayloads?: Record; /** CLI override (`--harness`). Wins over `flags`. */ cliHarness?: Harness; /** CLI override (`--sequence`). Wins over `flags`. */ @@ -43,6 +45,37 @@ export interface SwitchboardCtx { trace?: SwitchboardTrace; } +/** A resolver middleware: defer via `next()`, or assert by returning a value. */ +export type Middleware = (ctx: SwitchboardCtx, next: () => D) => D; + +/** + * Run a middleware chain over `ctx`. Each middleware receives `next` (which + * runs the rest of the chain) and can either: + * - defer: call `next()` and optionally modify its result (overlay pattern) + * - short-circuit: return a value without calling `next()` (skip the rest) + * + * **Earlier in the array = higher precedence.** Index 0 runs first and can + * short-circuit the rest; index 1 only runs if index 0 deferred. So + * `[cliSequenceMw, orchestratorFeatureFlagMw]` means CLI takes precedence over the + * flag, not the other way around. + * + * `fallback` runs at the end — reached only when every middleware deferred. + * Typically the map read for the base value. + */ +export function runChain( + chain: Middleware[], + ctx: SwitchboardCtx, + fallback: () => D, +): D { + function step(index: number): D { + if (index >= chain.length) return fallback(); + const middleware = chain[index]; + const next = () => step(index + 1); + return middleware(ctx, next); + } + return step(0); +} + // ── Data model ────────────────────────────────────────────────────────── /** Harness + model for one leaf of agent work. */ @@ -69,11 +102,92 @@ export interface ProgramBinding { contextMillOverride?: Record>; } +// Legacy fallback; new programs should explicitly choose Pi and prefer orchestration. +export const DEFAULT_BINDING: ProgramBinding = { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_SOL_MODEL, + thinkingLevel: 'medium', +}; + +/** + * Per-program routing. Kept in lockstep with `PROGRAM_REGISTRY` by the + * switchboard test. Anything absent falls back to `DEFAULT_BINDING`. + */ +export const PROGRAM_BINDINGS: Partial> = { + 'posthog-integration': DEFAULT_BINDING, + 'revenue-analytics-setup': DEFAULT_BINDING, + 'warehouse-source': DEFAULT_BINDING, + 'error-tracking-upload-source-maps': { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_SOL_MODEL, + thinkingLevel: 'medium', + }, + audit: DEFAULT_BINDING, + 'events-audit': DEFAULT_BINDING, + 'posthog-doctor': DEFAULT_BINDING, + 'web-analytics-doctor': DEFAULT_BINDING, + migration: DEFAULT_BINDING, + 'self-driving': DEFAULT_BINDING, + 'agent-skill': DEFAULT_BINDING, + 'mcp-add': DEFAULT_BINDING, + 'mcp-remove': DEFAULT_BINDING, + 'mcp-tutorial': DEFAULT_BINDING, + 'mcp-analytics': DEFAULT_BINDING, + // Orchestrator on pi. The binding routes only; every stage's model and + // effort are pinned context-mill side in the flow's frontmatter + // (`model_pi`/`effort_pi`: terra seed, sol tasks, luna report). + metrics: { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, + 'replay-vision': { + sequence: Sequence.orchestrator, + harness: Harness.anthropic, + model: DEFAULT_AGENT_MODEL, + }, + // Orchestrator on pi, like metrics. The binding routes only; every stage's + // model and effort are pinned context-mill side in the flow's frontmatter + // (`model_pi`/`effort_pi`: terra seed, install and init, sol tasks, luna report). + 'error-tracking': { + sequence: Sequence.orchestrator, + harness: Harness.pi, + model: DEFAULT_AGENT_MODEL, + }, + 'ai-observability': { + sequence: Sequence.linear, + harness: Harness.pi, + model: GPT5_6_TERRA_MODEL, + thinkingLevel: 'high', + }, + slack: DEFAULT_BINDING, +}; + +// ── Unified resolver ──────────────────────────────────────────────────── + +/** Compose both axes. Callers needing only one axis use the per-axis resolver. */ +export function resolveBinding( + ctx: SwitchboardCtx, + role = 'default', +): ProgramBinding { + ctx.trace ??= {}; + const sequence = resolveSequence(ctx); + const { harness, model, thinkingLevel } = resolveHarness(ctx, role); + return { sequence, harness, model, thinkingLevel }; +} + // ── Unified re-export surface ─────────────────────────────────────────── -export { HARNESS_OPTIONS, getHarness } from './harness'; +export { HARNESS_OPTIONS, getHarness, resolveHarness } from './harness'; +export { + SEQUENCE_OPTIONS, + getSequence, + resolveSequence, + type SequenceRunner, +} from './sequence'; export { - harnessRunsTasks, - resolveHarness, - resolveRoleHarness, -} from './resolve-harness'; -export { SEQUENCE_OPTIONS, getSequence, type SequenceRunner } from './sequence'; + isOrchestratorEnabled, + areSeededTasksEnabled, + resolveStageOverrides, +} from './flags'; diff --git a/src/agent/runner/switchboard/resolve-harness.ts b/src/agent/runner/switchboard/resolve-harness.ts deleted file mode 100644 index af47f4fa5..000000000 --- a/src/agent/runner/switchboard/resolve-harness.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Harness axis: the resolver that picks a harness and model, and which - * harnesses the orchestrator can drive. Data and pure functions only, so the - * agent entry loads this at startup; the registry is `harness.ts`. - */ - -import { IS_PRODUCTION_BUILD } from '@env'; -import { Harness } from '@shared/constants'; -import { logToFile } from '@utils/debug'; -import { DEFAULT_AGENT_BINDING } from '@agent/default-binding'; -import type { ResolvedBinding } from '../shared/types'; -import type { HarnessPick, ProgramBinding, SwitchboardCtx } from '.'; - -/** Which backends implement `runTask`; a registry test keeps this in step with HARNESS_OPTIONS. */ -export const HARNESS_RUNS_TASKS: Record = { - [Harness.anthropic]: true, - [Harness.pi]: true, -}; - -/** Whether the orchestrator can drive this harness. */ -export function harnessRunsTasks(name: Harness): boolean { - return HARNESS_RUNS_TASKS[name] === true; -} - -/** - * Resolve the harness for a role. Linear callers omit `role`; orchestrator - * callers pass `'seed'` or `task.type`. `contextMillOverride[role]` overlays. - * Precedence: CLI > flag route > binding default; the CLI overrides are - * dev/test only and gated out of published builds. - */ -export function resolveHarness( - ctx: SwitchboardCtx, - role = 'default', -): HarnessPick { - if (ctx.trace) - Object.assign(ctx.trace, { harness: 'binding', model: 'binding' }); - const binding: ProgramBinding = ctx.baseBinding ?? DEFAULT_AGENT_BINDING; - let pick: HarnessPick = { - harness: binding.harness, - model: binding.model, - thinkingLevel: binding.thinkingLevel, - ...binding.contextMillOverride?.[role], - }; - const route = ctx.flagRoute; - if (route) { - if (ctx.trace) { - ctx.trace.harness = 'flag'; - // Harness-only routes keep the binding's model — trace it truthfully so - // analytics never attributes the fallback model to the flag. - if (route.model) ctx.trace.model = 'flag'; - } - pick = { - harness: route.harness ?? Harness.pi, - model: route.model ?? pick.model, - thinkingLevel: route.thinkingLevel ?? pick.thinkingLevel, - }; - } - if (!IS_PRODUCTION_BUILD && ctx.cliModel) { - if (ctx.trace) ctx.trace.model = 'cli'; - pick = { ...pick, model: ctx.cliModel }; - } - if (!IS_PRODUCTION_BUILD && ctx.cliHarness) { - if (ctx.trace) ctx.trace.harness = 'cli'; - pick = { ...pick, harness: ctx.cliHarness }; - } - logToFile( - `[switchboard] resolved: program=${ctx.program ?? '?'} harness=${ - pick.harness - }` + - `${ctx.trace?.harness ? ` (${ctx.trace.harness})` : ''} model=${ - pick.model - }` + - `${ctx.trace?.model ? ` (${ctx.trace.model})` : ''}`, - ); - return pick; -} - -/** The agent resolves a task role only from data the caller already supplied. */ -export function resolveRoleHarness( - binding: ResolvedBinding, - role: string, -): HarnessPick { - return ( - binding.roleBindings?.[role] ?? { - harness: binding.harness, - model: binding.model, - thinkingLevel: binding.thinkingLevel, - } - ); -} diff --git a/src/agent/runner/switchboard/sequence.ts b/src/agent/runner/switchboard/sequence.ts index 43881f268..b6d241720 100644 --- a/src/agent/runner/switchboard/sequence.ts +++ b/src/agent/runner/switchboard/sequence.ts @@ -1,11 +1,27 @@ /** - * Sequence axis: the registry. Programs resolve which sequence a run uses. + * Sequence axis: gate helpers, registry, middleware, resolver. + * Percentage rollouts are PostHog-side — the gate just reads the resolved bool. */ +import { IS_PRODUCTION_BUILD } from '@env'; import { Sequence } from '@shared/constants'; +import { logToFile } from '@utils/debug'; +import { + isOrchestratorEnabled, + resolveFlagRoute, + resolveFlagSequence, +} from './flags'; +import { getHarness, resolveHarness } from './harness'; import type { SequenceResult, SequenceContext } from '../shared/types'; import { runLinearProgram } from '../sequence/linear'; import { runOrchestrator } from '../sequence/orchestrator/orchestrator-runner'; +import { + DEFAULT_BINDING, + PROGRAM_BINDINGS, + runChain, + type Middleware, + type SwitchboardCtx, +} from '.'; // ── Registry ──────────────────────────────────────────────────────────── @@ -33,3 +49,82 @@ export function getSequence(name: Sequence): SequenceRunner { } return sequence; } + +// ── Middleware + resolver ─────────────────────────────────────────────── + +/** + * A composed sub-run (integration inside self-driving) is structurally + * linear: the orchestrator owns the full run lifecycle (queue, outro) and + * cannot nest. Sits above every override, including CLI. + */ +const composedClampMw: Middleware = (ctx, next) => { + if (!ctx.composed) return next(); + if (ctx.trace) ctx.trace.sequence = 'composed'; + return Sequence.linear; +}; + +/** `--sequence` override. Dev/test only — the option is gated out of published builds. */ +const cliSequenceMw: Middleware = (ctx, next) => { + if (!ctx.cliSequence) return next(); + if (ctx.trace) ctx.trace.sequence = 'cli'; + return ctx.cliSequence; +}; + +/** A program's own flag route may pin the sequence; wins over the global orchestrator flag. Traced as 'payload' to stay distinguishable from sequence experiments ('flag'). */ +const flagRouteSequenceMw: Middleware = (ctx, next) => { + const route = resolveFlagRoute(ctx.program, ctx.flags, ctx.flagPayloads); + if (!route?.sequence) return next(); + if (ctx.trace) ctx.trace.sequence = 'payload'; + return route.sequence; +}; + +/** Sequence experiments (e.g. wizard-orchestrator), each inert outside its declared programs. */ +const sequenceExperimentMw: Middleware = (ctx, next) => { + const sequence = resolveFlagSequence(ctx.program, ctx.flags); + if (!sequence) return next(); + if (ctx.trace) ctx.trace.sequence = 'flag'; + return sequence; +}; + +/** + * The orchestrator drives harnesses through `runTask`; a harness that has not + * implemented it clamps the run to linear. A capability check, not a harness + * identity check — a harness gains orchestrator support by implementing the + * method, with no switchboard change. Sits below the CLI override so + * `--sequence orchestrator` still reproduces the hard error in dev builds. + */ +const runTaskCapabilityClampMw: Middleware = (ctx, next) => { + const pick = resolveHarness(ctx); + if (getHarness(pick.harness).runTask) return next(); + if (isOrchestratorEnabled(ctx.flags)) { + logToFile( + `[switchboard] wizard-orchestrator ignored: ${pick.harness} has no runTask, clamping to linear`, + ); + } + if (ctx.trace) ctx.trace.sequence = 'runtask-clamp'; + return Sequence.linear; +}; + +// Order = precedence: CLI > capability clamp > flag > binding default. The +// prod spread collapses to [], dropping cliSequenceMw from the chain. +const SEQUENCE_MIDDLEWARE: Middleware[] = [ + composedClampMw, + ...(IS_PRODUCTION_BUILD ? [] : [cliSequenceMw]), + runTaskCapabilityClampMw, + flagRouteSequenceMw, + sequenceExperimentMw, +]; + +/** CLI wins over `wizard-orchestrator` flag wins over binding default. */ +export function resolveSequence(ctx: SwitchboardCtx): Sequence { + const sequence = runChain(SEQUENCE_MIDDLEWARE, ctx, () => { + if (ctx.trace) ctx.trace.sequence = 'binding'; + const binding = PROGRAM_BINDINGS[ctx.program] ?? DEFAULT_BINDING; + return binding.sequence; + }); + logToFile( + `[switchboard] resolved: program=${ctx.program} sequence=${sequence}` + + `${ctx.trace?.sequence ? ` (${ctx.trace.sequence})` : ''}`, + ); + return sequence; +} diff --git a/src/agent/tools/tool-names.ts b/src/agent/tools/tool-names.ts deleted file mode 100644 index 40853267d..000000000 --- a/src/agent/tools/tool-names.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** Tool ids programs name in allowedTools and disallowedTools. Data only. */ - -export const SERVER_NAME = 'wizard-tools'; - -/** Tool names exposed by the wizard-tools server, keyed for selective use. */ -// SDK expects MCP tool names in allowedTools/disallowedTools to be the -// fully-qualified `mcp____` form (sdk.d.ts: "Fully-qualified -// MCP tool name, e.g. mcp__server__tool_name."). The colon form silently -// fails to match, which made every program's `disallowedTools` entry a no-op. -export const WIZARD_TOOL_NAMES = { - checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`, - setEnvValues: `mcp__${SERVER_NAME}__set_env_values`, - detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`, - loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`, - installSkill: `mcp__${SERVER_NAME}__install_skill`, - auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`, - auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`, - auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`, - wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`, - publishHandoff: `mcp__${SERVER_NAME}__publish_handoff`, - enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`, - completeTask: `mcp__${SERVER_NAME}__complete_task`, - readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`, -} as const; diff --git a/src/agent/tools/tools.ts b/src/agent/tools/tools.ts index 0ab9a6283..6751232a7 100644 --- a/src/agent/tools/tools.ts +++ b/src/agent/tools/tools.ts @@ -1156,7 +1156,28 @@ export function appendAuditChecksToLedger( return { ok: true, added: additions.length }; } -export { SERVER_NAME, WIZARD_TOOL_NAMES } from './tool-names'; +export const SERVER_NAME = 'wizard-tools'; + +/** Tool names exposed by the wizard-tools server, keyed for selective use. */ +// SDK expects MCP tool names in allowedTools/disallowedTools to be the +// fully-qualified `mcp____` form (sdk.d.ts: "Fully-qualified +// MCP tool name, e.g. mcp__server__tool_name."). The colon form silently +// fails to match, which made every program's `disallowedTools` entry a no-op. +export const WIZARD_TOOL_NAMES = { + checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`, + setEnvValues: `mcp__${SERVER_NAME}__set_env_values`, + detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`, + loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`, + installSkill: `mcp__${SERVER_NAME}__install_skill`, + auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`, + auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`, + auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`, + wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`, + publishHandoff: `mcp__${SERVER_NAME}__publish_handoff`, + enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`, + completeTask: `mcp__${SERVER_NAME}__complete_task`, + readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`, +} as const; // --------------------------------------------------------------------------- // Test-only exports diff --git a/src/agent/types.ts b/src/agent/types.ts index 94bce6df4..e3476645b 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -12,11 +12,8 @@ export type { AgentRunDefinition, PromptContext, RunConfig, - ResolvedBinding, - RunFlags, RunInput, RunResult, - SeedTaskEntry, } from './runner'; export type { AgentInteraction, @@ -31,9 +28,8 @@ export type { TokenUsageDelta, } from './progress'; -/** Input types of the exported resolveHarness. */ +/** Leaves in B2 with the bindings table; B1 deferred PROGRAM_BINDINGS. */ export type { ProgramBinding, SwitchboardCtx } from './runner'; -export type { EffortLevel } from './runner/switchboard/models'; /** Leaves in B2 with downloadSkill. */ export type { InstallSkillResult } from './tools'; diff --git a/src/agent/wizard-ask-bridge.ts b/src/agent/wizard-ask-bridge.ts index e03b5c91b..5c57a1b79 100644 --- a/src/agent/wizard-ask-bridge.ts +++ b/src/agent/wizard-ask-bridge.ts @@ -92,7 +92,13 @@ export const CANCELLED_SENTINEL = '__cancelled__'; /** Default per-question timeout (5 minutes). */ export const DEFAULT_ASK_TIMEOUT_MS = 5 * 60 * 1000; -export { LONGER_ASK_TIMEOUT_MS } from '@shared/ask-policy'; +/** + * The longer per-question timeout, for asks that send the user on an errand — + * open a database console, mint a restricted API key. The default above is + * sized for a question answerable from memory and expires long before an + * errand is done. + */ +export const LONGER_ASK_TIMEOUT_MS = 20 * 60 * 1000; function buildCancelledAnswers(questions: AskQuestion[]): AskAnswers { const out: AskAnswers = {}; diff --git a/src/shared/file-watcher.ts b/src/lib/file-watcher.ts similarity index 100% rename from src/shared/file-watcher.ts rename to src/lib/file-watcher.ts diff --git a/src/lib/runners/__tests__/mint-recovery.test.ts b/src/lib/runners/__tests__/mint-recovery.test.ts index 25d35c7a2..ada5cf6c2 100644 --- a/src/lib/runners/__tests__/mint-recovery.test.ts +++ b/src/lib/runners/__tests__/mint-recovery.test.ts @@ -1,6 +1,6 @@ import { vi, it, expect, afterEach } from 'vitest'; import { runWizard } from '../run-wizard'; -import { runProgramAgent } from '../run-program-agent'; +import { runProgramAgent } from '@programs/run-agent-legacy'; import { startTUI } from '@ui/tui/start-tui'; import { WizardStore } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; @@ -10,7 +10,7 @@ import { ScreenId } from '@ui/tui/router'; import { HostResolution } from '@shared/host-resolution'; import { analytics } from '@utils/analytics'; -vi.mock('../run-program-agent', () => ({ runProgramAgent: vi.fn() })); +vi.mock('@programs/run-agent-legacy', () => ({ runProgramAgent: vi.fn() })); vi.mock('@ui/tui/start-tui', () => ({ startTUI: vi.fn() })); vi.mock('@shared/local-dev', async (original) => ({ ...(await original()), diff --git a/src/lib/runners/run-non-interactive.ts b/src/lib/runners/run-non-interactive.ts index 7ddb16154..308532649 100644 --- a/src/lib/runners/run-non-interactive.ts +++ b/src/lib/runners/run-non-interactive.ts @@ -337,7 +337,7 @@ export function runNonInteractive( } } - const { runProgramAgent } = await import('./run-program-agent'); + const { runProgramAgent } = await import('@programs/run-agent-legacy'); await runProgramAgent(config, session); await settleStream(RunPhase.Completed); } catch (error) { diff --git a/src/lib/runners/run-wizard.ts b/src/lib/runners/run-wizard.ts index ab12530d6..2ef2ff9cf 100644 --- a/src/lib/runners/run-wizard.ts +++ b/src/lib/runners/run-wizard.ts @@ -1,6 +1,6 @@ import { VERSION } from '@shared/version'; import { logToFile, getLogFilePath } from '@utils/debug'; -import { runProgramAgent } from './run-program-agent'; +import { runProgramAgent } from '@programs/run-agent-legacy'; import { authenticate } from '@programs/authenticate'; import { getProgramConfig } from '@programs'; import { getAuditChecks } from '@programs/audit/types'; diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 2d9a0dca2..4739c3aa6 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -11,7 +11,6 @@ */ import { POSTHOG_LOCAL_URL, resolveLocalDev } from '@shared/local-dev'; -import { DiscoveredFeature } from '@shared/scan-consent'; import { AdditionalFeature, ADDITIONAL_FEATURE_LABELS, @@ -68,7 +67,11 @@ export enum RunPhase { Error = 'error', } -export { DiscoveredFeature }; +/** Features discovered by the feature-discovery subagent */ +export enum DiscoveredFeature { + Stripe = 'stripe', + LLM = 'llm', +} /** Consent to report what local detection found (see `scanConsent` below). */ export enum ScanConsent { @@ -455,8 +458,21 @@ export function buildSession(args: { }; } -export { - mayReportScanResults, - reportableDiscoveredFeatures, - reportablePosthogSdkDetected, -} from '@shared/scan-consent'; +/** One place to ask, so a new consent state does not need three edits. */ +export function mayReportScanResults(session: WizardSession): boolean { + return session.scanConsent === ScanConsent.Granted; +} + +/** Lives here so analytics infrastructure never learns what consent means. */ +export function reportableDiscoveredFeatures( + session: WizardSession, +): DiscoveredFeature[] | undefined { + return mayReportScanResults(session) ? session.discoveredFeatures : undefined; +} + +/** Also a scan result, so it travels under the same consent as the rest. */ +export function reportablePosthogSdkDetected( + session: WizardSession, +): boolean | undefined { + return mayReportScanResults(session) ? session.posthogSdkDetected : undefined; +} diff --git a/src/programs/__tests__/run-agent-legacy.test.ts b/src/programs/__tests__/run-agent-legacy.test.ts index e99cb4ab4..efa72b0ea 100644 --- a/src/programs/__tests__/run-agent-legacy.test.ts +++ b/src/programs/__tests__/run-agent-legacy.test.ts @@ -1,7 +1,7 @@ import { runNonInteractive } from '@lib/runners/run-non-interactive'; import { runWizard } from '@lib/runners/run-wizard'; import { authenticate } from '@programs/authenticate'; -import { runProgramAgent } from '@lib/runners/run-program-agent'; +import { runProgramAgent } from '../run-agent-legacy'; import { runAgent, RunOutcome, type RunResult } from '@agent/runner'; import { Harness, Sequence } from '@shared/constants'; import { buildSession, OutroKind } from '@lib/wizard-session'; diff --git a/src/programs/agent-skill/index.ts b/src/programs/agent-skill/index.ts index 7632d30cf..75752731e 100644 --- a/src/programs/agent-skill/index.ts +++ b/src/programs/agent-skill/index.ts @@ -20,14 +20,36 @@ */ import type { ProgramConfig } from '@programs/program-step'; +import type { AbortCase } from '@agent/types'; +import type { ProgramRun } from '@programs/program-run'; import { AGENT_SKILL_STEPS } from './steps.js'; import { getContentBlocks } from '../../ui/tui/decks/agent-skill/index.js'; -import { - skillRunDefinition, - type SkillProgramOptions, -} from './run-definition.js'; -export type { SkillProgramOptions } from './run-definition.js'; +export interface SkillProgramOptions { + /** Context-mill skill ID to install */ + skillId: string; + /** CLI subcommand name */ + command: string; + /** Unique flow key — must match a Program enum entry */ + id: string; + /** CLI description shown in --help */ + description: string; + /** Analytics integration label */ + integrationLabel: string; + /** Custom prompt instruction. Appended after default project prompt. */ + customPrompt?: string; + successMessage: string; + reportFile: string; + docsUrl: string; + spinnerMessage: string; + estimatedDurationMinutes: number; + /** Other program ids that must be satisfied first */ + requires?: string[]; + /** Override the default outro. Receives the same args as ProgramRun.buildOutroData. */ + buildOutroData?: ProgramRun['buildOutroData']; + /** Known `[ABORT] ` cases the skill can emit. */ + abortCases?: AbortCase[]; +} export function createSkillProgram(opts: SkillProgramOptions): ProgramConfig { return { @@ -38,7 +60,18 @@ export function createSkillProgram(opts: SkillProgramOptions): ProgramConfig { steps: AGENT_SKILL_STEPS, reportFile: opts.reportFile, getContentBlocks, - run: skillRunDefinition(opts), + run: { + skillId: opts.skillId, + integrationLabel: opts.integrationLabel, + customPrompt: opts.customPrompt ? () => opts.customPrompt! : undefined, + successMessage: opts.successMessage, + reportFile: opts.reportFile, + docsUrl: opts.docsUrl, + spinnerMessage: opts.spinnerMessage, + estimatedDurationMinutes: opts.estimatedDurationMinutes, + buildOutroData: opts.buildOutroData, + abortCases: opts.abortCases, + }, requires: opts.requires, }; } diff --git a/src/programs/agent-skill/run-definition.ts b/src/programs/agent-skill/run-definition.ts deleted file mode 100644 index 6a13a9570..000000000 --- a/src/programs/agent-skill/run-definition.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AbortCase } from '@agent/types'; -import type { ProgramRun } from '@programs/program-run'; - -export interface SkillProgramOptions { - /** Context-mill skill ID to install */ - skillId: string; - /** CLI subcommand name */ - command: string; - /** Unique flow key — must match a Program enum entry */ - id: string; - /** CLI description shown in --help */ - description: string; - /** Analytics integration label */ - integrationLabel: string; - /** Custom prompt instruction. Appended after default project prompt. */ - customPrompt?: string; - successMessage: string; - reportFile: string; - docsUrl: string; - spinnerMessage: string; - estimatedDurationMinutes: number; - /** Other program ids that must be satisfied first */ - requires?: string[]; - /** Override the default outro. Receives the same args as ProgramRun.buildOutroData. */ - buildOutroData?: ProgramRun['buildOutroData']; - /** Known `[ABORT] ` cases the skill can emit. */ - abortCases?: AbortCase[]; -} - -export function skillRunDefinition(opts: SkillProgramOptions): ProgramRun { - return { - skillId: opts.skillId, - integrationLabel: opts.integrationLabel, - customPrompt: opts.customPrompt ? () => opts.customPrompt! : undefined, - successMessage: opts.successMessage, - reportFile: opts.reportFile, - docsUrl: opts.docsUrl, - spinnerMessage: opts.spinnerMessage, - estimatedDurationMinutes: opts.estimatedDurationMinutes, - buildOutroData: opts.buildOutroData, - abortCases: opts.abortCases, - }; -} diff --git a/src/programs/ai-observability/index.ts b/src/programs/ai-observability/index.ts index 68b08030c..b71697d30 100644 --- a/src/programs/ai-observability/index.ts +++ b/src/programs/ai-observability/index.ts @@ -2,12 +2,13 @@ import type { ProgramConfig, ProgramStep } from '@programs/program-step'; import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; import { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; import { headlessOption, regionOption } from '@lib/headless-mode'; -import { AI_OBSERVABILITY_REPORT_FILE, AI_OBSERVABILITY_RUN } from './run.js'; const AI_OBSERVABILITY_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => step.id === 'intro' ? { ...step, screenId: 'ai-observability-intro' } : step, ); +const AI_OBSERVABILITY_REPORT_FILE = 'posthog-ai-observability-report.md'; + /** * `wizard ai-observability` — wrap the project's LLM client calls so they emit * `$ai_generation` events into AI Observability. @@ -26,5 +27,38 @@ export const aiObservabilityConfig: ProgramConfig = { steps: AI_OBSERVABILITY_STEPS, reportFile: AI_OBSERVABILITY_REPORT_FILE, getContentBlocks, - run: AI_OBSERVABILITY_RUN, + run: { + integrationLabel: 'ai-observability', + // No `skillId`: linear.ts skips its pre-install step (see the gate on + // `linear.ts:47`), so the agent must load the menu and install the right + // variant itself. The prompt below tells it how. + customPrompt: + () => `Instrument this project's LLM calls with PostHog AI Observability. + +This flow has no pre-installed skill — you install the right one yourself: + +1. Call \`load_skill_menu\` with \`category: "ai-observability"\`. The menu is + the source of truth: one variant per (LLM provider × language), plus a + \`manual-capture\` variant for projects with no vendor SDK. + +2. Scan the project manifest (\`package.json\`, \`pyproject.toml\`, + \`requirements.txt\`) for a vendor LLM SDK and pick the variant that matches + it — the language follows the manifest (\`package.json\` → Node, Python + tooling → Python). Multiple SDKs (e.g. LangChain wrapping OpenAI) → prefer + the higher abstraction. No vendor SDK → the \`manual-capture\` variant. + Genuinely ambiguous → \`wizard_ask\` with a multi-choice picker. + +3. Call \`install_skill\` with the picked variant id. Then follow that skill's + \`SKILL.md\` and references end-to-end. The skill itself will install + packages, wire OTel, set env vars, and describe verification. + +Make only additive changes — do not touch existing PostHog init, identify +calls, event capture, or dashboards. Those belong to other skills. The +final report is written to ./${AI_OBSERVABILITY_REPORT_FILE}.`, + successMessage: `AI Observability configured! View the report at ./${AI_OBSERVABILITY_REPORT_FILE}`, + reportFile: AI_OBSERVABILITY_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/ai-observability', + spinnerMessage: 'Setting up AI Observability...', + estimatedDurationMinutes: 5, + }, }; diff --git a/src/programs/ai-observability/run.ts b/src/programs/ai-observability/run.ts deleted file mode 100644 index 1e9510e42..000000000 --- a/src/programs/ai-observability/run.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ProgramRun } from '@programs/program-run'; - -export const AI_OBSERVABILITY_REPORT_FILE = - 'posthog-ai-observability-report.md'; - -export const AI_OBSERVABILITY_RUN: ProgramRun = { - integrationLabel: 'ai-observability', - // No `skillId`: linear.ts skips its pre-install step (see the gate on - // `linear.ts:47`), so the agent must load the menu and install the right - // variant itself. The prompt below tells it how. - customPrompt: - () => `Instrument this project's LLM calls with PostHog AI Observability. - -This flow has no pre-installed skill — you install the right one yourself: - -1. Call \`load_skill_menu\` with \`category: "ai-observability"\`. The menu is - the source of truth: one variant per (LLM provider × language), plus a - \`manual-capture\` variant for projects with no vendor SDK. - -2. Scan the project manifest (\`package.json\`, \`pyproject.toml\`, - \`requirements.txt\`) for a vendor LLM SDK and pick the variant that matches - it — the language follows the manifest (\`package.json\` → Node, Python - tooling → Python). Multiple SDKs (e.g. LangChain wrapping OpenAI) → prefer - the higher abstraction. No vendor SDK → the \`manual-capture\` variant. - Genuinely ambiguous → \`wizard_ask\` with a multi-choice picker. - -3. Call \`install_skill\` with the picked variant id. Then follow that skill's - \`SKILL.md\` and references end-to-end. The skill itself will install - packages, wire OTel, set env vars, and describe verification. - -Make only additive changes — do not touch existing PostHog init, identify -calls, event capture, or dashboards. Those belong to other skills. The -final report is written to ./${AI_OBSERVABILITY_REPORT_FILE}.`, - successMessage: `AI Observability configured! View the report at ./${AI_OBSERVABILITY_REPORT_FILE}`, - reportFile: AI_OBSERVABILITY_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/ai-observability', - spinnerMessage: 'Setting up AI Observability...', - estimatedDurationMinutes: 5, -}; diff --git a/src/programs/audit/ledger-watcher.ts b/src/programs/audit/ledger-watcher.ts index cf0c0f441..f7a312543 100644 --- a/src/programs/audit/ledger-watcher.ts +++ b/src/programs/audit/ledger-watcher.ts @@ -10,7 +10,7 @@ import { startFileWatcher, type FileWatcherHandle, type FileWatcherOptions, -} from '@shared/file-watcher'; +} from '@lib/file-watcher'; import { logToFile } from '@utils/debug'; import { AUDIT_CHECKS_KEY, coerceAuditChecks } from './types.js'; diff --git a/src/programs/binding-telemetry.ts b/src/programs/binding-telemetry.ts deleted file mode 100644 index fa6a53bef..000000000 --- a/src/programs/binding-telemetry.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Sequence, - WIZARD_ORCHESTRATOR_FLAG_KEY, - WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, -} from '@shared/constants'; -import type { ResolvedBinding } from '@agent/types'; -import { analytics } from '@utils/analytics'; -import { logToFile } from '@utils/debug'; -import type { ProgramSwitchboardCtx } from './binding'; - -/** Record the selected route and its precedence sources once per program run. */ -export function captureSwitchboardDecision( - ctx: ProgramSwitchboardCtx, - binding: ResolvedBinding, -): void { - const trace = ctx.trace ?? {}; - const perTaskModel = - binding.sequence === Sequence.orchestrator && trace.model === 'binding'; - const model = perTaskModel ? 'chosen-per-task' : binding.model; - const modelSource = perTaskModel ? 'agent-prompts' : trace.model; - analytics.wizardCapture('switchboard resolved', { - program: ctx.program, - flag_self_driving_use_pi_harness: - ctx.flags[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY], - flag_self_driving_pi_payload: JSON.stringify( - ctx.flagPayloads?.[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY] ?? null, - ), - flag_orchestrator: ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY], - cli_harness: ctx.cliHarness, - cli_sequence: ctx.cliSequence, - cli_model: ctx.cliModel, - harness_source: trace.harness, - model_source: modelSource, - sequence_source: trace.sequence, - harness: binding.harness, - model, - thinking_level: binding.thinkingLevel, - sequence: binding.sequence, - }); - logToFile( - `[switchboard] decision: program=${ctx.program}` + - ` in(orchestrator=${ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY] ?? '-'},` + - ` cli=${ctx.cliHarness ?? '-'}/${ctx.cliSequence ?? '-'}/${ - ctx.cliModel ?? '-' - })` + - ` → harness=${binding.harness} (${trace.harness ?? '?'})` + - ` model=${model} (${modelSource ?? '?'})` + - ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, - ); -} diff --git a/src/programs/binding.ts b/src/programs/binding.ts deleted file mode 100644 index 59c30b348..000000000 --- a/src/programs/binding.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { IS_PRODUCTION_BUILD } from '@env'; -import { - DEFAULT_AGENT_MODEL, - GPT5_6_SOL_MODEL, - GPT5_6_TERRA_MODEL, - Harness, - Sequence, -} from '@shared/constants'; -import { logToFile } from '@utils/debug'; -import { - DEFAULT_AGENT_BINDING, - harnessRunsTasks, - resolveHarness, -} from '@agent'; -import type { - ProgramBinding, - ResolvedBinding, - SwitchboardCtx as HarnessCtx, -} from '@agent/types'; -import type { ProgramId } from './program-registry'; -import { - isOrchestratorEnabled, - resolveFlagRoute, - resolveFlagSequence, -} from './experiments'; - -/** The agent's harness inputs plus the sequence inputs only programs read. */ -type SwitchboardCtx = HarnessCtx & { - flagSequence?: Sequence; - orchestratorFlagOn?: boolean; -}; - -export interface ProgramSwitchboardCtx { - program: ProgramId; - composed?: boolean; - flags: Record; - flagPayloads?: Record; - cliHarness?: Harness; - cliSequence?: Sequence; - cliModel?: string; - trace?: SwitchboardCtx['trace']; -} - -/** Program routes. The registry lockstep contract is tested at this boundary. */ -export const PROGRAM_BINDINGS: Partial> = { - 'posthog-integration': DEFAULT_AGENT_BINDING, - 'revenue-analytics-setup': DEFAULT_AGENT_BINDING, - 'warehouse-source': DEFAULT_AGENT_BINDING, - 'error-tracking-upload-source-maps': { - sequence: Sequence.linear, - harness: Harness.pi, - model: GPT5_6_SOL_MODEL, - thinkingLevel: 'medium', - }, - audit: DEFAULT_AGENT_BINDING, - 'events-audit': DEFAULT_AGENT_BINDING, - 'posthog-doctor': DEFAULT_AGENT_BINDING, - 'web-analytics-doctor': DEFAULT_AGENT_BINDING, - migration: DEFAULT_AGENT_BINDING, - 'self-driving': DEFAULT_AGENT_BINDING, - 'agent-skill': DEFAULT_AGENT_BINDING, - 'mcp-add': DEFAULT_AGENT_BINDING, - 'mcp-remove': DEFAULT_AGENT_BINDING, - 'mcp-tutorial': DEFAULT_AGENT_BINDING, - 'mcp-analytics': DEFAULT_AGENT_BINDING, - metrics: { - sequence: Sequence.orchestrator, - harness: Harness.pi, - model: DEFAULT_AGENT_MODEL, - }, - 'replay-vision': { - sequence: Sequence.orchestrator, - harness: Harness.anthropic, - model: DEFAULT_AGENT_MODEL, - }, - 'error-tracking': { - sequence: Sequence.orchestrator, - harness: Harness.pi, - model: DEFAULT_AGENT_MODEL, - }, - 'ai-observability': { - sequence: Sequence.linear, - harness: Harness.pi, - model: GPT5_6_TERRA_MODEL, - thinkingLevel: 'high', - }, - slack: DEFAULT_AGENT_BINDING, -}; - -/** Resolve product policy once; the agent receives only the resulting route. */ -export function resolveProgramBinding( - ctx: ProgramSwitchboardCtx, -): ResolvedBinding { - ctx.trace ??= {}; - const baseBinding: ProgramBinding = - PROGRAM_BINDINGS[ctx.program] ?? DEFAULT_AGENT_BINDING; - const resolution = { - program: ctx.program, - baseBinding, - composed: ctx.composed, - flagRoute: resolveFlagRoute(ctx.program, ctx.flags, ctx.flagPayloads), - flagSequence: resolveFlagSequence(ctx.program, ctx.flags), - orchestratorFlagOn: isOrchestratorEnabled(ctx.flags), - cliHarness: ctx.cliHarness, - cliSequence: ctx.cliSequence, - cliModel: ctx.cliModel, - trace: ctx.trace, - }; - const sequence = resolveSequence(resolution); - const { harness, model, thinkingLevel } = resolveHarness(resolution); - const binding = { sequence, harness, model, thinkingLevel }; - const roles = Object.keys(baseBinding.contextMillOverride ?? {}); - if (roles.length === 0) return binding; - return { - ...binding, - roleBindings: Object.fromEntries( - roles.map((role) => [ - role, - resolveHarness({ ...resolution, trace: undefined }, role), - ]), - ), - }; -} - -function resolveSequence(ctx: SwitchboardCtx): Sequence { - const [source, sequence] = pickSequence(ctx); - if (ctx.trace) ctx.trace.sequence = source; - logToFile( - `[switchboard] resolved: program=${ - ctx.program ?? '?' - } sequence=${sequence} (${source})`, - ); - return sequence; -} - -/** - * The first rung that decides wins: the composed clamp, the dev-build CLI - * override, the runTask capability clamp, the flag route, the experiment, then - * the base binding. CLI sits above the capability clamp, so `--sequence - * orchestrator` still reaches the orchestrator's hard error in dev builds. - */ -function pickSequence( - ctx: SwitchboardCtx, -): [Required>['sequence'], Sequence] { - // The orchestrator owns the whole run lifecycle and cannot nest. - if (ctx.composed) return ['composed', Sequence.linear]; - if (!IS_PRODUCTION_BUILD && ctx.cliSequence) return ['cli', ctx.cliSequence]; - const { harness } = resolveHarness(ctx); - if (!harnessRunsTasks(harness)) { - if (ctx.orchestratorFlagOn) { - logToFile( - `[switchboard] wizard-orchestrator ignored: ${harness} has no runTask, clamping to linear`, - ); - } - return ['runtask-clamp', Sequence.linear]; - } - if (ctx.flagRoute?.sequence) return ['payload', ctx.flagRoute.sequence]; - if (ctx.flagSequence) return ['flag', ctx.flagSequence]; - return ['binding', (ctx.baseBinding ?? DEFAULT_AGENT_BINDING).sequence]; -} diff --git a/src/programs/commandments.ts b/src/programs/commandments.ts deleted file mode 100644 index d89d267de..000000000 --- a/src/programs/commandments.ts +++ /dev/null @@ -1,18 +0,0 @@ -// ── Program axis ──────────────────────────────────────────────────────── - -const SELF_DRIVING = [ - 'ALWAYS surface a custom-scout proposal in step 6b: bring the user your one or two strongest candidate scouts even when the built-in troop looks sufficient. The proposal ask leads with a "None — keep the built-in troop" option, so declining costs the user one keystroke — but a proposal you silently skip is coverage they never got to see or judge. Where the skill says to skip the ask when the gap analysis finds no candidate, do NOT skip: pick your best candidates anyway and let the user decide.', - - 'Rank candidates at the discriminator level, not the category level. "Covered" only means an enabled scout would actually FIRE for that failure mode: a conversion-rate watcher does not catch entry volume collapsing; a Stripe-transaction watcher does not catch a lead form going silent. A surface whose failure mode has no firing condition among the enabled scouts is your strongest candidate.', - - 'Be honest in the option descriptions: if a candidate overlaps something an enabled scout partially watches, say so in its description rather than dropping the candidate. The user chooses with full information; you do not gatekeep on their behalf.', -]; - -const PROGRAM_COMMANDMENTS: Record = { - 'self-driving': SELF_DRIVING, -}; - -/** Select program-specific guidance before invoking the agent. */ -export function getProgramCommandments(program: string): readonly string[] { - return PROGRAM_COMMANDMENTS[program] ?? []; -} diff --git a/src/programs/mcp-analytics/index.ts b/src/programs/mcp-analytics/index.ts index c8b4bba50..faa9afd1c 100644 --- a/src/programs/mcp-analytics/index.ts +++ b/src/programs/mcp-analytics/index.ts @@ -1,7 +1,44 @@ +import type { AbortCase } from '@agent/types'; +import { ErrorCodes } from '@shared/errors'; import { createSkillProgram } from '@programs/agent-skill/index'; -import { MCP_ANALYTICS_OPTIONS } from './run.js'; -export { MCP_ANALYTICS_ABORT_CASES } from './run.js'; +const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; + +/** + * `[ABORT]` reasons the mcp-analytics skill emits when the project can't be + * instrumented. Kept in sync with the stop conditions in the skill's + * `description.md` (context-mill `context/skills/mcp-analytics`). + */ +export const MCP_ANALYTICS_ABORT_CASES: AbortCase[] = [ + { + match: /^unsupported language for mcp analytics$/i, + errorCode: ErrorCodes.DetectUnsupportedPlatform, + message: 'Unsupported language for MCP analytics', + body: + 'MCP analytics supports TypeScript/JavaScript (`@posthog/mcp`) and Python ' + + '(`posthog.mcp`, shipped inside the `posthog` package). This project ' + + "doesn't look like either, so there's nothing to instrument. " + + 'See https://posthog.com/docs/mcp-analytics for the supported setups.', + }, + { + match: /^no mcp server found$/i, + message: 'No MCP server found', + body: + 'This command instruments an existing MCP server with PostHog analytics, ' + + 'but no MCP server was found in this project. If you just want PostHog ' + + 'product analytics, run `npx @posthog/wizard` instead.', + }, + { + match: /^could not locate the server entry point$/i, + message: 'Could not locate the MCP server entry point', + body: + "This project has MCP signals, but the agent couldn't find where the " + + "server is constructed or requests are dispatched, so there's nowhere " + + 'safe to add instrumentation. See https://posthog.com/docs/mcp-analytics ' + + 'for the supported server styles, or point the wizard at the package ' + + "that defines the server if it's in a monorepo subdirectory.", + }, +]; /** * `wizard mcp-analytics` — flat skill command. @@ -17,4 +54,23 @@ export { MCP_ANALYTICS_ABORT_CASES } from './run.js'; * 'mcp-analytics'` from context-mill — a deliberate breaking change, done then, * not pre-emptively. */ -export const mcpAnalyticsConfig = createSkillProgram(MCP_ANALYTICS_OPTIONS); +export const mcpAnalyticsConfig = createSkillProgram({ + skillId: 'mcp-analytics', + command: 'mcp-analytics', + id: 'mcp-analytics', + description: 'Add PostHog MCP Analytics to your MCP server', + integrationLabel: 'mcp-analytics', + customPrompt: + "Instrument this project's MCP server with PostHog MCP analytics. Run the " + + '`mcp-analytics` skill end-to-end: detect the server style, install ' + + '`@posthog/mcp` and `posthog-node`, wrap the server (or use `PostHogMCP` ' + + 'for a custom dispatcher), wire the project API key and host, and verify. ' + + 'Make only additive changes — do not alter tool behavior. The final report ' + + `is written to ./${MCP_ANALYTICS_REPORT_FILE}.`, + successMessage: `MCP analytics configured! View the report at ./${MCP_ANALYTICS_REPORT_FILE}`, + reportFile: MCP_ANALYTICS_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/mcp-analytics', + spinnerMessage: 'Setting up MCP analytics...', + estimatedDurationMinutes: 5, + abortCases: MCP_ANALYTICS_ABORT_CASES, +}); diff --git a/src/programs/mcp-analytics/run.ts b/src/programs/mcp-analytics/run.ts deleted file mode 100644 index 690a31291..000000000 --- a/src/programs/mcp-analytics/run.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { AbortCase } from '@agent/types'; -import { ErrorCodes } from '@shared/errors'; -import type { SkillProgramOptions } from '@programs/agent-skill/run-definition'; - -const MCP_ANALYTICS_REPORT_FILE = 'posthog-mcp-analytics-report.md'; - -/** - * `[ABORT]` reasons the mcp-analytics skill emits when the project can't be - * instrumented. Kept in sync with the stop conditions in the skill's - * `description.md` (context-mill `context/skills/mcp-analytics`). - */ -export const MCP_ANALYTICS_ABORT_CASES: AbortCase[] = [ - { - match: /^unsupported language for mcp analytics$/i, - errorCode: ErrorCodes.DetectUnsupportedPlatform, - message: 'Unsupported language for MCP analytics', - body: - 'MCP analytics supports TypeScript/JavaScript (`@posthog/mcp`) and Python ' + - '(`posthog.mcp`, shipped inside the `posthog` package). This project ' + - "doesn't look like either, so there's nothing to instrument. " + - 'See https://posthog.com/docs/mcp-analytics for the supported setups.', - }, - { - match: /^no mcp server found$/i, - message: 'No MCP server found', - body: - 'This command instruments an existing MCP server with PostHog analytics, ' + - 'but no MCP server was found in this project. If you just want PostHog ' + - 'product analytics, run `npx @posthog/wizard` instead.', - }, - { - match: /^could not locate the server entry point$/i, - message: 'Could not locate the MCP server entry point', - body: - "This project has MCP signals, but the agent couldn't find where the " + - "server is constructed or requests are dispatched, so there's nowhere " + - 'safe to add instrumentation. See https://posthog.com/docs/mcp-analytics ' + - 'for the supported server styles, or point the wizard at the package ' + - "that defines the server if it's in a monorepo subdirectory.", - }, -]; - -export const MCP_ANALYTICS_OPTIONS: SkillProgramOptions = { - skillId: 'mcp-analytics', - command: 'mcp-analytics', - id: 'mcp-analytics', - description: 'Add PostHog MCP Analytics to your MCP server', - integrationLabel: 'mcp-analytics', - customPrompt: - "Instrument this project's MCP server with PostHog MCP analytics. Run the " + - '`mcp-analytics` skill end-to-end: detect the server style, install ' + - '`@posthog/mcp` and `posthog-node`, wrap the server (or use `PostHogMCP` ' + - 'for a custom dispatcher), wire the project API key and host, and verify. ' + - 'Make only additive changes — do not alter tool behavior. The final report ' + - `is written to ./${MCP_ANALYTICS_REPORT_FILE}.`, - successMessage: `MCP analytics configured! View the report at ./${MCP_ANALYTICS_REPORT_FILE}`, - reportFile: MCP_ANALYTICS_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/mcp-analytics', - spinnerMessage: 'Setting up MCP analytics...', - estimatedDurationMinutes: 5, - abortCases: MCP_ANALYTICS_ABORT_CASES, -}; diff --git a/src/programs/metrics/index.ts b/src/programs/metrics/index.ts index 3cce288e5..c96612ae3 100644 --- a/src/programs/metrics/index.ts +++ b/src/programs/metrics/index.ts @@ -1,12 +1,13 @@ import type { ProgramConfig, ProgramStep } from '@programs/program-step'; import { AGENT_SKILL_STEPS } from '@programs/agent-skill/index'; import { getContentBlocks } from '@ui/tui/decks/agent-skill/index'; -import { METRICS_REPORT_FILE, METRICS_RUN } from './run.js'; const METRICS_STEPS: ProgramStep[] = AGENT_SKILL_STEPS.map((step) => step.id === 'intro' ? { ...step, screenId: 'metrics-intro' } : step, ); +const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; + /** * `wizard metrics` — instrument the project with PostHog application metrics * (`posthog.metrics` counters, gauges, and histograms). @@ -29,5 +30,45 @@ export const metricsConfig: ProgramConfig = { steps: METRICS_STEPS, reportFile: METRICS_REPORT_FILE, getContentBlocks, - run: METRICS_RUN, + run: { + integrationLabel: 'metrics', + // No `skillId`: the agent must load the menu and install the right + // variant itself. The prompt below tells it how. + customPrompt: + () => `Instrument this project with PostHog application metrics. + +This flow has no pre-installed skill — you install the right one yourself: + +1. Call \`load_skill_menu\` with \`category: "metrics"\`. The menu is the + source of truth: one variant per platform. + +2. Pick the variant that matches the project: + - Python tooling (\`pyproject.toml\`, \`requirements.txt\`, \`Pipfile\`) → + \`metrics-python\` (needs \`posthog\` >= 7.23.0) + - \`package.json\` with server-side Node code → \`metrics-nodejs\` + (needs \`posthog-node\` >= 5.43.0) + - \`package.json\` that is browser-only → \`metrics-javascript\` + (needs \`posthog-js\` >= 1.399.0) + - Kubernetes manifests / Helm charts and the user wants cluster-level + scraping → \`metrics-kubernetes\` + - Any other language → \`metrics-other\` (plain OTLP exporter) + A full-stack app (e.g. Next.js) usually wants the server variant — metrics + measure service work, not user actions. Genuinely ambiguous → + \`wizard_ask\` with a multi-choice picker. + +3. Call \`install_skill\` with the picked variant id. Then follow that skill's + \`SKILL.md\` and references end-to-end — it covers where to place metrics + (middleware, background jobs, external calls, business commit sites) and + the low-cardinality attribute rules. + +Make only additive changes — reuse an existing PostHog client by adding the +\`metrics\` config to it rather than constructing a second client, and do not +touch existing identify calls, event capture, or dashboards. The final report +is written to ./${METRICS_REPORT_FILE}.`, + successMessage: `Application metrics configured! View the report at ./${METRICS_REPORT_FILE}`, + reportFile: METRICS_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/metrics', + spinnerMessage: 'Setting up application metrics...', + estimatedDurationMinutes: 5, + }, }; diff --git a/src/programs/metrics/run.ts b/src/programs/metrics/run.ts deleted file mode 100644 index 295cc2455..000000000 --- a/src/programs/metrics/run.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ProgramRun } from '@programs/program-run'; - -export const METRICS_REPORT_FILE = 'posthog-metrics-report.md'; - -export const METRICS_RUN: ProgramRun = { - integrationLabel: 'metrics', - // No `skillId`: the agent must load the menu and install the right - // variant itself. The prompt below tells it how. - customPrompt: () => `Instrument this project with PostHog application metrics. - -This flow has no pre-installed skill — you install the right one yourself: - -1. Call \`load_skill_menu\` with \`category: "metrics"\`. The menu is the - source of truth: one variant per platform. - -2. Pick the variant that matches the project: - - Python tooling (\`pyproject.toml\`, \`requirements.txt\`, \`Pipfile\`) → - \`metrics-python\` (needs \`posthog\` >= 7.23.0) - - \`package.json\` with server-side Node code → \`metrics-nodejs\` - (needs \`posthog-node\` >= 5.43.0) - - \`package.json\` that is browser-only → \`metrics-javascript\` - (needs \`posthog-js\` >= 1.399.0) - - Kubernetes manifests / Helm charts and the user wants cluster-level - scraping → \`metrics-kubernetes\` - - Any other language → \`metrics-other\` (plain OTLP exporter) - A full-stack app (e.g. Next.js) usually wants the server variant — metrics - measure service work, not user actions. Genuinely ambiguous → - \`wizard_ask\` with a multi-choice picker. - -3. Call \`install_skill\` with the picked variant id. Then follow that skill's - \`SKILL.md\` and references end-to-end — it covers where to place metrics - (middleware, background jobs, external calls, business commit sites) and - the low-cardinality attribute rules. - -Make only additive changes — reuse an existing PostHog client by adding the -\`metrics\` config to it rather than constructing a second client, and do not -touch existing identify calls, event capture, or dashboards. The final report -is written to ./${METRICS_REPORT_FILE}.`, - successMessage: `Application metrics configured! View the report at ./${METRICS_REPORT_FILE}`, - reportFile: METRICS_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/metrics', - spinnerMessage: 'Setting up application metrics...', - estimatedDurationMinutes: 5, -}; diff --git a/src/programs/migration/index.ts b/src/programs/migration/index.ts index 81f0db084..907160f6d 100644 --- a/src/programs/migration/index.ts +++ b/src/programs/migration/index.ts @@ -1,12 +1,28 @@ import type { ProgramConfig } from '@programs/program-step'; +import type { AbortCase } from '@agent/types'; import { WIZARD_TOOL_NAMES } from '@agent'; import { MIGRATION_PROGRAM } from './steps.js'; import { getContentBlocks } from '../../ui/tui/decks/migration/index.js'; -import { - DEFAULT_MIGRATE_SKILL_ID, - MIGRATION_REPORT_FILE, - MIGRATION_RUN, -} from './run.js'; + +const MIGRATION_REPORT_FILE = 'migration-report.md'; + +const MIGRATION_ABORT_CASES: AbortCase[] = [ + { + match: /^no source-sdk calls found$/i, + message: 'No source-SDK calls found', + body: + 'The migration needs an existing third-party SDK to migrate from. No ' + + 'calls to the source SDK appear anywhere in this project. If you ' + + "haven't installed PostHog yet, you don't need this command — run " + + '`npx @posthog/wizard@latest` to add PostHog from scratch.', + }, +]; + +// Default skill id when nothing else picks one. The `wizard migrate ` +// subcommands override this via skillCommandFactory using each manifest +// entry's skillId, so this default only kicks in for legacy callers (e.g. +// programmatic uses of migrationConfig directly). +const DEFAULT_MIGRATE_SKILL_ID = 'migrate-statsig'; export const migrationConfig: ProgramConfig = { command: 'migrate', @@ -18,7 +34,23 @@ export const migrationConfig: ProgramConfig = { getContentBlocks, allowedTools: ['Agent'], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], - run: MIGRATION_RUN, + run: { + skillId: DEFAULT_MIGRATE_SKILL_ID, + integrationLabel: 'migration', + customPrompt: () => + 'Migrate this project from its existing third-party analytics, ' + + 'feature-flag, and observability tools to PostHog. Run the `migrate` ' + + 'skill end-to-end: follow the step chain starting at ' + + 'references/1-presence.md. Only replace existing source-SDK call sites ' + + 'with PostHog equivalents — make zero unrelated changes and no ' + + `net-new instrumentation. The final report is written to ./${MIGRATION_REPORT_FILE}.`, + successMessage: `Migration complete! View the report at ./${MIGRATION_REPORT_FILE}`, + reportFile: MIGRATION_REPORT_FILE, + docsUrl: '', + spinnerMessage: 'Migrating to PostHog...', + estimatedDurationMinutes: 8, + abortCases: MIGRATION_ABORT_CASES, + }, requires: ['posthog-integration'], }; diff --git a/src/programs/migration/run.ts b/src/programs/migration/run.ts deleted file mode 100644 index 25a28f14d..000000000 --- a/src/programs/migration/run.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { AbortCase } from '@agent/types'; -import type { ProgramRun } from '@programs/program-run'; - -export const MIGRATION_REPORT_FILE = 'migration-report.md'; - -const MIGRATION_ABORT_CASES: AbortCase[] = [ - { - match: /^no source-sdk calls found$/i, - message: 'No source-SDK calls found', - body: - 'The migration needs an existing third-party SDK to migrate from. No ' + - 'calls to the source SDK appear anywhere in this project. If you ' + - "haven't installed PostHog yet, you don't need this command — run " + - '`npx @posthog/wizard@latest` to add PostHog from scratch.', - }, -]; - -// Default skill id when nothing else picks one. The `wizard migrate ` -// subcommands override this via skillCommandFactory using each manifest -// entry's skillId, so this default only kicks in for legacy callers (e.g. -// programmatic uses of migrationConfig directly). -export const DEFAULT_MIGRATE_SKILL_ID = 'migrate-statsig'; - -export const MIGRATION_RUN: ProgramRun = { - skillId: DEFAULT_MIGRATE_SKILL_ID, - integrationLabel: 'migration', - customPrompt: () => - 'Migrate this project from its existing third-party analytics, ' + - 'feature-flag, and observability tools to PostHog. Run the `migrate` ' + - 'skill end-to-end: follow the step chain starting at ' + - 'references/1-presence.md. Only replace existing source-SDK call sites ' + - 'with PostHog equivalents — make zero unrelated changes and no ' + - `net-new instrumentation. The final report is written to ./${MIGRATION_REPORT_FILE}.`, - successMessage: `Migration complete! View the report at ./${MIGRATION_REPORT_FILE}`, - reportFile: MIGRATION_REPORT_FILE, - docsUrl: '', - spinnerMessage: 'Migrating to PostHog...', - estimatedDurationMinutes: 8, - abortCases: MIGRATION_ABORT_CASES, -}; diff --git a/src/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts index 3f72530c3..e1d91c70f 100644 --- a/src/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -1,5 +1,5 @@ import type { ProgramConfig, ProgramStep } from '@programs/program-step'; -import { runProgramAgent } from '@lib/runners/run-program-agent'; +import { runProgramAgent } from '@programs/run-agent-legacy'; import type { ProgramRun } from '@programs/program-run'; import { AgentSignals, shouldDisableAsk, WIZARD_TOOL_NAMES } from '@agent'; import type { WizardSession } from '@lib/wizard-session'; diff --git a/src/programs/program-run.ts b/src/programs/program-run.ts index b11a394eb..a3d9a15be 100644 --- a/src/programs/program-run.ts +++ b/src/programs/program-run.ts @@ -1,7 +1,7 @@ /** * A program's run definition: the agent's `AgentRunDefinition` plus the * completion hooks that read the session. The agent never calls these — - * `src/lib/runners/run-program-agent.ts` binds them to the run's credentials and hands the + * `run-agent-legacy.ts` binds them to the run's credentials and hands the * agent `RunConfig.hooks`. */ diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index 2c6561d97..314bfc9a4 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -1,3 +1,4 @@ +import type { AbortCase } from '@agent/types'; import { Integration } from '@shared/constants'; import { detectFramework, @@ -17,9 +18,8 @@ import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; -import { REPLAY_VISION_OPTIONS } from './run.js'; -export { REPLAY_VISION_ABORT_CASES } from './run.js'; +const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; /** * The platforms session replay can actually record on. Replay vision watches @@ -77,6 +77,22 @@ async function abortUnsupportedPlatform( }); } +/** + * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. + * Kept in sync with the stop conditions in the skill's `description.md` + * (context-mill `context/skills/replay-vision`). + */ +export const REPLAY_VISION_ABORT_CASES: AbortCase[] = [ + { + match: /^replay vision not available for this project$/i, + message: 'Replay vision is not available for this project', + body: + 'Every Replay vision scanner endpoint reported that the feature is not ' + + 'available here yet. Session replay setup done so far is kept. See ' + + 'https://posthog.com/docs/replay-vision for availability.', + }, +]; + /** * Framework detection ahead of the run, exactly like the default integration * program. The orchestrator requires it: `session.skillId` must hold the @@ -103,7 +119,32 @@ const DETECT_STEP: ProgramStep = { }, }; -const base = createSkillProgram(REPLAY_VISION_OPTIONS); +const base = createSkillProgram({ + // The menu ids this skill `-`, and context-mill's + // `replay-vision/config.yaml` declares a single variant, `setup`. The bare + // `replay-vision` id does not exist — the orchestrator never installs this + // (it resolves per-task mini-skills instead), but the linear path does, and + // aborts `skill-not-found` on a miss. + skillId: 'replay-vision-setup', + command: 'replay-vision', + id: 'replay-vision', + description: 'Set up PostHog Replay Vision scanners for your product', + integrationLabel: 'replay-vision', + customPrompt: + 'Set up PostHog Replay vision. Run the `replay-vision` skill end-to-end: ' + + 'make sure session replay is recording (server-side enable plus a ' + + 'posthog-js init check), then create the vision scanners the skill ' + + "defines, scoped to this product's key flows read out of the repo. If " + + 'PostHog is not integrated yet, install and initialize the SDK first as ' + + 'the skill instructs — do not abort. The final report is written to ' + + `./${REPLAY_VISION_REPORT_FILE}.`, + successMessage: `Replay vision configured! View the report at ./${REPLAY_VISION_REPORT_FILE}`, + reportFile: REPLAY_VISION_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/replay-vision', + spinnerMessage: 'Setting up Replay vision...', + estimatedDurationMinutes: 6, + abortCases: REPLAY_VISION_ABORT_CASES, +}); /** * `wizard replay-vision` — flat skill command on the orchestrator sequence. diff --git a/src/programs/replay-vision/run.ts b/src/programs/replay-vision/run.ts deleted file mode 100644 index b8eaca446..000000000 --- a/src/programs/replay-vision/run.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { AbortCase } from '@agent/types'; -import type { SkillProgramOptions } from '@programs/agent-skill/run-definition'; - -const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; - -/** - * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. - * Kept in sync with the stop conditions in the skill's `description.md` - * (context-mill `context/skills/replay-vision`). - */ -export const REPLAY_VISION_ABORT_CASES: AbortCase[] = [ - { - match: /^replay vision not available for this project$/i, - message: 'Replay vision is not available for this project', - body: - 'Every Replay vision scanner endpoint reported that the feature is not ' + - 'available here yet. Session replay setup done so far is kept. See ' + - 'https://posthog.com/docs/replay-vision for availability.', - }, -]; - -export const REPLAY_VISION_OPTIONS: SkillProgramOptions = { - // The menu ids this skill `-`, and context-mill's - // `replay-vision/config.yaml` declares a single variant, `setup`. The bare - // `replay-vision` id does not exist — the orchestrator never installs this - // (it resolves per-task mini-skills instead), but the linear path does, and - // aborts `skill-not-found` on a miss. - skillId: 'replay-vision-setup', - command: 'replay-vision', - id: 'replay-vision', - description: 'Set up PostHog Replay Vision scanners for your product', - integrationLabel: 'replay-vision', - customPrompt: - 'Set up PostHog Replay vision. Run the `replay-vision` skill end-to-end: ' + - 'make sure session replay is recording (server-side enable plus a ' + - 'posthog-js init check), then create the vision scanners the skill ' + - "defines, scoped to this product's key flows read out of the repo. If " + - 'PostHog is not integrated yet, install and initialize the SDK first as ' + - 'the skill instructs — do not abort. The final report is written to ' + - `./${REPLAY_VISION_REPORT_FILE}.`, - successMessage: `Replay vision configured! View the report at ./${REPLAY_VISION_REPORT_FILE}`, - reportFile: REPLAY_VISION_REPORT_FILE, - docsUrl: 'https://posthog.com/docs/replay-vision', - spinnerMessage: 'Setting up Replay vision...', - estimatedDurationMinutes: 6, - abortCases: REPLAY_VISION_ABORT_CASES, -}; diff --git a/src/programs/revenue-analytics/abort-cases.ts b/src/programs/revenue-analytics/abort-cases.ts deleted file mode 100644 index abb917a32..000000000 --- a/src/programs/revenue-analytics/abort-cases.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { AbortCase } from '@agent/types'; - -/** `[ABORT] ` cases the revenue analytics skill can emit. */ -export const REVENUE_ABORT_CASES: AbortCase[] = [ - { - // Skill emits: [ABORT] Could not find a PostHog distinct_id - match: /^could not find a posthog distinct_id$/i, - message: 'Could not find a PostHog distinct_id', - body: - 'The agent could not find PostHog distinct_id usage in your codebase. ' + - 'Your users must be identified in PostHog before they can be tagged in Stripe. ' + - 'Please identify your users and try again.', - docsUrl: 'https://posthog.com/docs/product-analytics/identify', - }, - { - // Skill emits: [ABORT] Could not find a Stripe integration - match: /^could not find a stripe integration$/i, - message: 'Could not find a Stripe integration', - body: - 'The Wizard could not find an existing Stripe customer, charge, ' + - 'subscription, or other Stripe operations. Please run the Revenue ' + - 'Analytics Wizard on a project with an existing Stripe integration.', - docsUrl: 'https://posthog.com/docs/revenue-analytics', - }, -]; diff --git a/src/programs/revenue-analytics/detect.ts b/src/programs/revenue-analytics/detect.ts index 380a40628..b81090ffd 100644 --- a/src/programs/revenue-analytics/detect.ts +++ b/src/programs/revenue-analytics/detect.ts @@ -7,6 +7,7 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; +import type { AbortCase } from '@agent/types'; import { findPackageJsons } from '@programs/shared/package-scanning'; export { @@ -31,7 +32,29 @@ export type RevenueDetectError = | { kind: 'missing-posthog'; foundStripe: string[] } | { kind: 'missing-stripe'; foundPosthog: string[] }; -export { REVENUE_ABORT_CASES } from './abort-cases.js'; +/** `[ABORT] ` cases the revenue analytics skill can emit. */ +export const REVENUE_ABORT_CASES: AbortCase[] = [ + { + // Skill emits: [ABORT] Could not find a PostHog distinct_id + match: /^could not find a posthog distinct_id$/i, + message: 'Could not find a PostHog distinct_id', + body: + 'The agent could not find PostHog distinct_id usage in your codebase. ' + + 'Your users must be identified in PostHog before they can be tagged in Stripe. ' + + 'Please identify your users and try again.', + docsUrl: 'https://posthog.com/docs/product-analytics/identify', + }, + { + // Skill emits: [ABORT] Could not find a Stripe integration + match: /^could not find a stripe integration$/i, + message: 'Could not find a Stripe integration', + body: + 'The Wizard could not find an existing Stripe customer, charge, ' + + 'subscription, or other Stripe operations. Please run the Revenue ' + + 'Analytics Wizard on a project with an existing Stripe integration.', + docsUrl: 'https://posthog.com/docs/revenue-analytics', + }, +]; /** * Scan `session.installDir` for PostHog + Stripe SDKs. Writes detection diff --git a/src/programs/revenue-analytics/index.ts b/src/programs/revenue-analytics/index.ts index ac321880b..4fae357ac 100644 --- a/src/programs/revenue-analytics/index.ts +++ b/src/programs/revenue-analytics/index.ts @@ -1,8 +1,8 @@ import type { ProgramConfig } from '@programs/program-step'; import { WIZARD_TOOL_NAMES } from '@agent'; import { REVENUE_ANALYTICS_PROGRAM } from './steps.js'; +import { REVENUE_ABORT_CASES } from './detect.js'; import { getContentBlocks } from '../../ui/tui/decks/revenue-analytics/index.js'; -import { REVENUE_ANALYTICS_RUN } from './run.js'; export const revenueAnalyticsConfig: ProgramConfig = { command: 'revenue-analytics', @@ -13,7 +13,17 @@ export const revenueAnalyticsConfig: ProgramConfig = { getContentBlocks, allowedTools: ['Agent'], disallowedTools: [WIZARD_TOOL_NAMES.wizardAsk], - run: REVENUE_ANALYTICS_RUN, + run: { + skillId: 'revenue-analytics-setup', + integrationLabel: 'revenue-analytics-setup', + customPrompt: () => 'Set up revenue analytics for this project.', + successMessage: 'Revenue analytics configured!', + reportFile: 'posthog-revenue-report.md', + docsUrl: 'https://posthog.com/docs/revenue-analytics', + spinnerMessage: 'Setting up revenue analytics...', + estimatedDurationMinutes: 5, + abortCases: REVENUE_ABORT_CASES, + }, requires: ['posthog-integration'], }; diff --git a/src/programs/revenue-analytics/run.ts b/src/programs/revenue-analytics/run.ts deleted file mode 100644 index 861c75992..000000000 --- a/src/programs/revenue-analytics/run.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ProgramRun } from '@programs/program-run'; -import { REVENUE_ABORT_CASES } from './abort-cases.js'; - -export const REVENUE_ANALYTICS_RUN: ProgramRun = { - skillId: 'revenue-analytics-setup', - integrationLabel: 'revenue-analytics-setup', - customPrompt: () => 'Set up revenue analytics for this project.', - successMessage: 'Revenue analytics configured!', - reportFile: 'posthog-revenue-report.md', - docsUrl: 'https://posthog.com/docs/revenue-analytics', - spinnerMessage: 'Setting up revenue analytics...', - estimatedDurationMinutes: 5, - abortCases: REVENUE_ABORT_CASES, -}; diff --git a/src/lib/runners/run-program-agent.ts b/src/programs/run-agent-legacy.ts similarity index 85% rename from src/lib/runners/run-program-agent.ts rename to src/programs/run-agent-legacy.ts index 92f2b102d..b343cccf6 100644 --- a/src/lib/runners/run-program-agent.ts +++ b/src/programs/run-agent-legacy.ts @@ -21,22 +21,18 @@ import { createUiReducer, uiInteraction } from '@ui/agent-progress'; import { buildRunTags, flushScanReport, + resolveBinding, runAgent, RunOutcome, TASK_OUTCOMES_KEY, } from '@agent'; -import type { RunConfig, RunInput } from '@agent/types'; -import { - resolveProgramBinding, - type ProgramSwitchboardCtx, -} from '@programs/binding'; -import { getProgramCommandments } from '@programs/commandments'; -import { captureSwitchboardDecision } from '@programs/binding-telemetry'; -import { - areSeededTasksEnabled, - resolveStageOverrides, -} from '@programs/experiments'; -import type { ProgramRun } from '@programs/program-run'; +import type { + ProgramBinding, + RunConfig, + RunInput, + SwitchboardCtx, +} from '@agent/types'; +import type { ProgramRun } from './program-run'; import { backupAndFixClaudeSettings, checkAllSettingsConflicts, @@ -57,16 +53,15 @@ import { isNonInteractiveEnvironment } from '@utils/environment'; import { getSkillsBaseUrl, Sequence, + WIZARD_ORCHESTRATOR_FLAG_KEY, + WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, type Integration, } from '@shared/constants'; import { FRAMEWORK_REGISTRY } from '@programs/registry'; -import { postAuthGateSteps, type ProgramConfig } from '@programs/program-step'; -import { - authenticate, - refreshAccessTokenIfNeeded, -} from '@programs/authenticate'; -import { maybeStampAiSdkDetected } from '@programs/posthog-integration/detect'; -import { startAuditLedgerWatcher } from '@programs/audit/ledger-watcher'; +import { postAuthGateSteps, type ProgramConfig } from './program-step'; +import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; +import { maybeStampAiSdkDetected } from './posthog-integration/detect'; +import { startAuditLedgerWatcher } from './audit/ledger-watcher'; /** * Resolve a ProgramConfig's agent run definition and execute the pipeline. @@ -186,7 +181,7 @@ async function runProgram( // Resolve which sequence and harness will run a program (CLI → PostHog flag → // per-program binding → default), tag both axes onto analytics, and hand the // binding to the agent for dispatch. - const switchboard: ProgramSwitchboardCtx = { + const switchboard: SwitchboardCtx = { program: programConfig.id, composed, flags: wizardFlags, @@ -195,7 +190,7 @@ async function runProgram( cliSequence: session.sequence, cliModel: session.model, }; - const binding = resolveProgramBinding(switchboard); + const binding = resolveBinding(switchboard); analytics.setTag('sequence', binding.sequence); analytics.setTag('harness', binding.harness); wizardMetadata.SEQUENCE = binding.sequence; @@ -226,13 +221,7 @@ async function runProgram( run, composed, binding, - programCommandments: getProgramCommandments(programConfig.id), - stageOverrides: resolveStageOverrides( - programConfig.id, - wizardFlags, - wizardFlagPayloads, - ), - seededTasksEnabled: areSeededTasksEnabled(wizardFlags), + switchboard, skillsBaseUrl: getSkillsBaseUrl(), wizardFlags, wizardFlagPayloads, @@ -451,3 +440,50 @@ async function runSettingsGate(session: WizardSession): Promise { logToFile('[agent-runner] settings override resolved'); } } + +// ── Switchboard telemetry ───────────────────────────────────────────── + +/** + * One event + one log line per run: what entered the switchboard, which + * precedence rung decided each axis, and the final pick. + */ +function captureSwitchboardDecision( + ctx: SwitchboardCtx, + binding: ProgramBinding, +): void { + const trace = ctx.trace ?? {}; + // Unpinned orchestrator runs choose a model per task from the context-mill agent prompts; the orchestrator logs that map once the prompts load. + const perTaskModel = + binding.sequence === Sequence.orchestrator && trace.model === 'binding'; + const model = perTaskModel ? 'chosen-per-task' : binding.model; + const modelSource = perTaskModel ? 'agent-prompts' : trace.model; + analytics.wizardCapture('switchboard resolved', { + program: ctx.program, + flag_self_driving_use_pi_harness: + ctx.flags[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY], + flag_self_driving_pi_payload: JSON.stringify( + ctx.flagPayloads?.[WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY] ?? null, + ), + flag_orchestrator: ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY], + cli_harness: ctx.cliHarness, + cli_sequence: ctx.cliSequence, + cli_model: ctx.cliModel, + harness_source: trace.harness, + model_source: modelSource, + sequence_source: trace.sequence, + harness: binding.harness, + model, + thinking_level: binding.thinkingLevel, + sequence: binding.sequence, + }); + logToFile( + `[switchboard] decision: program=${ctx.program}` + + ` in(orchestrator=${ctx.flags[WIZARD_ORCHESTRATOR_FLAG_KEY] ?? '-'},` + + ` cli=${ctx.cliHarness ?? '-'}/${ctx.cliSequence ?? '-'}/${ + ctx.cliModel ?? '-' + })` + + ` → harness=${binding.harness} (${trace.harness ?? '?'})` + + ` model=${model} (${modelSource ?? '?'})` + + ` sequence=${binding.sequence} (${trace.sequence ?? '?'})`, + ); +} diff --git a/src/programs/task-stream/event-plan-watcher.ts b/src/programs/task-stream/event-plan-watcher.ts index 7fadb0118..d04cee77c 100644 --- a/src/programs/task-stream/event-plan-watcher.ts +++ b/src/programs/task-stream/event-plan-watcher.ts @@ -3,7 +3,7 @@ import { startFileWatcher, type FileWatcherHandle, type FileWatcherOptions, -} from '@shared/file-watcher'; +} from '@lib/file-watcher'; const MAX_EVENT_PLAN_FILE_BYTES = 256 * 1024; const MAX_EVENT_COUNT = 50; diff --git a/src/programs/types.ts b/src/programs/types.ts index 5ac379afe..620e94d18 100644 --- a/src/programs/types.ts +++ b/src/programs/types.ts @@ -7,4 +7,3 @@ export type { StoreInitContext, } from './program-step'; export type { FrameworkConfig, SetupQuestion } from './framework-config'; -export type { ProgramSwitchboardCtx } from './binding'; diff --git a/src/programs/web-analytics-doctor/abort-cases.ts b/src/programs/web-analytics-doctor/abort-cases.ts deleted file mode 100644 index 7b5fbd129..000000000 --- a/src/programs/web-analytics-doctor/abort-cases.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { AbortCase } from '@agent/types'; -import { ErrorCodes } from '@shared/errors'; - -export const WEB_ANALYTICS_ABORT_CASES: AbortCase[] = [ - { - match: /^no web analytics events$/i, - message: 'No web analytics events', - body: - 'The doctor found no $pageview events in the last 30 days, so there is ' + - 'nothing to audit yet. Make sure PostHog is initialized and capturing ' + - 'pageviews, then run the doctor again.', - docsUrl: 'https://posthog.com/docs/web-analytics/getting-started', - }, - { - match: /^insufficient permissions$/i, - errorCode: ErrorCodes.AuthMissingScope, - message: 'Insufficient permissions', - body: - 'The doctor could not query your project — the authenticated token is ' + - 'missing query access. Re-run the wizard to sign in again, or use a key ' + - 'with read access to your events.', - docsUrl: 'https://posthog.com/docs/web-analytics', - }, - { - match: /^posthog sdk not installed$/i, - errorCode: ErrorCodes.DetectNoPosthogSdk, - message: 'PostHog SDK not installed', - body: - 'The doctor could not find a PostHog SDK in this project. Install and ' + - 'configure PostHog first (run `npx @posthog/wizard`), then run the ' + - 'doctor to check your web analytics setup.', - docsUrl: 'https://posthog.com/docs/libraries/js', - }, -]; diff --git a/src/programs/web-analytics-doctor/detect.ts b/src/programs/web-analytics-doctor/detect.ts index 6340ff6e9..e7df63673 100644 --- a/src/programs/web-analytics-doctor/detect.ts +++ b/src/programs/web-analytics-doctor/detect.ts @@ -1,5 +1,7 @@ import { existsSync, statSync } from 'fs'; import type { WizardSession } from '@lib/wizard-session'; +import type { AbortCase } from '@agent/types'; +import { ErrorCodes } from '@shared/errors'; import { findPackageJsons } from '@programs/shared/package-scanning'; export type WebAnalyticsDetectError = @@ -11,7 +13,37 @@ export type WebAnalyticsDetectError = | { kind: 'no-package-json' } | { kind: 'no-posthog'; scannedCount: number }; -export { WEB_ANALYTICS_ABORT_CASES } from './abort-cases.js'; +export const WEB_ANALYTICS_ABORT_CASES: AbortCase[] = [ + { + match: /^no web analytics events$/i, + message: 'No web analytics events', + body: + 'The doctor found no $pageview events in the last 30 days, so there is ' + + 'nothing to audit yet. Make sure PostHog is initialized and capturing ' + + 'pageviews, then run the doctor again.', + docsUrl: 'https://posthog.com/docs/web-analytics/getting-started', + }, + { + match: /^insufficient permissions$/i, + errorCode: ErrorCodes.AuthMissingScope, + message: 'Insufficient permissions', + body: + 'The doctor could not query your project — the authenticated token is ' + + 'missing query access. Re-run the wizard to sign in again, or use a key ' + + 'with read access to your events.', + docsUrl: 'https://posthog.com/docs/web-analytics', + }, + { + match: /^posthog sdk not installed$/i, + errorCode: ErrorCodes.DetectNoPosthogSdk, + message: 'PostHog SDK not installed', + body: + 'The doctor could not find a PostHog SDK in this project. Install and ' + + 'configure PostHog first (run `npx @posthog/wizard`), then run the ' + + 'doctor to check your web analytics setup.', + docsUrl: 'https://posthog.com/docs/libraries/js', + }, +]; export function detectWebAnalyticsPrerequisites( session: WizardSession, diff --git a/src/programs/web-analytics-doctor/index.ts b/src/programs/web-analytics-doctor/index.ts index a7c2863ce..4d11413c2 100644 --- a/src/programs/web-analytics-doctor/index.ts +++ b/src/programs/web-analytics-doctor/index.ts @@ -1,10 +1,33 @@ import type { ProgramConfig } from '@programs/program-step'; import { createSkillProgram } from '../agent-skill/index.js'; import { WEB_ANALYTICS_DOCTOR_PROGRAM } from './steps.js'; -import { WEB_ANALYTICS_DOCTOR_OPTIONS } from './run.js'; +import { WEB_ANALYTICS_ABORT_CASES } from './detect.js'; + +const REPORT_FILE = 'posthog-web-analytics-report.md'; +const DOCS_URL = 'https://posthog.com/docs/web-analytics'; export const webAnalyticsDoctorConfig: ProgramConfig = { - ...createSkillProgram(WEB_ANALYTICS_DOCTOR_OPTIONS), + ...createSkillProgram({ + skillId: 'web-analytics-doctor', + command: 'web-analytics', + id: 'web-analytics-doctor', + description: 'Audit and fix your PostHog web analytics setup', + integrationLabel: 'web-analytics-doctor', + customPrompt: + "Run the web-analytics-doctor skill to check this project's PostHog web " + + 'analytics setup. Audit read-only first, then present the findings to the ' + + 'user with a single wizard_ask multi-select and apply only the fixes they ' + + 'choose — editing project code and/or PostHog project settings via the ' + + 'MCP — before writing the report.', + successMessage: + 'Web analytics check complete! You can view the report at ./posthog-web-analytics-report.md', + reportFile: REPORT_FILE, + docsUrl: DOCS_URL, + spinnerMessage: 'Checking your web analytics setup...', + estimatedDurationMinutes: 5, + requires: ['posthog-integration'], + abortCases: WEB_ANALYTICS_ABORT_CASES, + }), steps: WEB_ANALYTICS_DOCTOR_PROGRAM, parentCommand: 'audit', }; diff --git a/src/programs/web-analytics-doctor/run.ts b/src/programs/web-analytics-doctor/run.ts deleted file mode 100644 index cd6bf6b44..000000000 --- a/src/programs/web-analytics-doctor/run.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { SkillProgramOptions } from '@programs/agent-skill/run-definition'; -import { WEB_ANALYTICS_ABORT_CASES } from './abort-cases.js'; - -const REPORT_FILE = 'posthog-web-analytics-report.md'; -const DOCS_URL = 'https://posthog.com/docs/web-analytics'; - -export const WEB_ANALYTICS_DOCTOR_OPTIONS: SkillProgramOptions = { - skillId: 'web-analytics-doctor', - command: 'web-analytics', - id: 'web-analytics-doctor', - description: 'Audit and fix your PostHog web analytics setup', - integrationLabel: 'web-analytics-doctor', - customPrompt: - "Run the web-analytics-doctor skill to check this project's PostHog web " + - 'analytics setup. Audit read-only first, then present the findings to the ' + - 'user with a single wizard_ask multi-select and apply only the fixes they ' + - 'choose — editing project code and/or PostHog project settings via the ' + - 'MCP — before writing the report.', - successMessage: - 'Web analytics check complete! You can view the report at ./posthog-web-analytics-report.md', - reportFile: REPORT_FILE, - docsUrl: DOCS_URL, - spinnerMessage: 'Checking your web analytics setup...', - estimatedDurationMinutes: 5, - requires: ['posthog-integration'], - abortCases: WEB_ANALYTICS_ABORT_CASES, -}; diff --git a/src/shared/ask-policy.ts b/src/shared/ask-policy.ts deleted file mode 100644 index 5266e499c..000000000 --- a/src/shared/ask-policy.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** When a run may put a `wizard_ask` question to a person, and how long it waits. */ - -/** - * Whether the `wizard_ask` overlay stays unwired for this run. Non-interactive - * modes (CI, signup) have no human to answer. Per-program disabling adds - * WIZARD_ASK_TOOL_NAME to the program's `disallowedTools` instead, so the SDK - * rejects calls outright. - * - * `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 keep the overlay unwired. - */ -export function isAskDisabled(flags: { - ci: boolean; - signup: boolean; - e2eAsk: boolean; -}): boolean { - return (flags.ci || flags.signup) && !flags.e2eAsk; -} - -/** - * The longer per-question timeout, for asks that send the user on an errand — - * open a database console, mint a restricted API key. The default is sized for - * a question answerable from memory and expires long before an errand is done. - */ -export const LONGER_ASK_TIMEOUT_MS = 20 * 60 * 1000; diff --git a/src/shared/gateway-auth.ts b/src/shared/gateway-auth.ts deleted file mode 100644 index f673c5eb2..000000000 --- a/src/shared/gateway-auth.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** Gateway bearer types and checks shared by the agent and its hosts. */ - -export interface GatewayAuth { - /** Base URL for model calls (no `/v1`; transports append their route). */ - gatewayUrl: string; - /** Gateway bearer, minted normally or supplied directly by CI. */ - token: string; - /** Team verified by the mint, or explicitly supplied for CI attribution. */ - teamId?: number; - /** - * Instant past which a 401 on this bearer is age rather than a bad - * credential: the cache re-mints past it, and a session still holding the - * old bearer may re-mint once. Before it the mint has to be trusted. - */ - refreshAtMs: number; -} - -/** Whether a 401 on this bearer may be age (past its refresh instant) rather than a bad credential. */ -export function isPastRefresh(auth: GatewayAuth, now = Date.now()): boolean { - return now >= auth.refreshAtMs; -} - -/** - * Whether a server-supplied origin may receive a bearer and prompt content: - * https (loopback excepted), and either a current cloud gateway or the host the run - * authenticated against. - */ -export function isTrustedGatewayUrl(value: string, apiHost: string): boolean { - let url: URL; - try { - url = new URL(value); - } catch { - return false; - } - // Consumers append routes to this value, so anything beyond an origin - // (path, query, fragment, userinfo) would build a malformed endpoint. - if ( - url.pathname !== '/' || - url.search || - url.hash || - url.username || - url.password - ) { - return false; - } - const localhost = - url.hostname === 'localhost' || - url.hostname === '127.0.0.1' || - url.hostname === 'host.docker.internal'; - // Loopback is the dev gateway, and is the one case allowed over http. - if (localhost) return true; - if (url.protocol !== 'https:') return false; - if (url.hostname.endsWith('.posthog.com')) { - return ( - url.origin === 'https://ai-gateway.us.posthog.com' || - url.origin === 'https://ai-gateway.eu.posthog.com' - ); - } - try { - return url.hostname === new URL(apiHost).hostname; - } catch { - return false; - } -} - -/** - * The v2 run-metadata carrier: one JSON blob for the `X-PostHog-Properties` - * header. Plain keys only, since the gateway strips `$`-prefixed keys as reserved, - * so feature-flag variants land as `wizard_flag_` instead of the legacy - * `$feature/` (dashboards keying on `$feature/wizard-*` read the new key - * post-cutover). - */ -export function buildWizardPropertiesBlob( - wizardMetadata: Record, - wizardFlags: Record, - teamId?: number, -): string { - // The gateway pins `$ai_product` to `wizard:`, and rejects a legacy - // product override on a scoped token, so the unprefixed key every cost and - // error consumer reads is only present if this blob declares it. - const props: Record = { ai_product: 'wizard' }; - if (teamId !== undefined) props.team_id = teamId; - for (const [key, value] of Object.entries(wizardMetadata)) { - props[stripPropertyPrefix(key)] = value; - } - for (const [flagKey, variant] of Object.entries(wizardFlags)) { - if (!flagKey.toLowerCase().startsWith('wizard')) continue; - props[`wizard_flag_${flagKey.toLowerCase()}`] = variant; - } - return JSON.stringify(props); -} - -const LEGACY_PROPERTY_PREFIX = 'X-POSTHOG-PROPERTY-'; - -function stripPropertyPrefix(key: string): string { - return key.toUpperCase().startsWith(LEGACY_PROPERTY_PREFIX) - ? key.slice(LEGACY_PROPERTY_PREFIX.length).toLowerCase() - : key; -} diff --git a/src/shared/run-tags.ts b/src/shared/run-tags.ts deleted file mode 100644 index 144d4b638..000000000 --- a/src/shared/run-tags.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CallType } from '@shared/constants'; - -/** - * Global identifiers attached to every LLM gateway trace for a run. They ride on - * each `$ai_generation` the gateway emits (in the `X-PostHog-Properties` blob - * `buildAgentEnv` builds), so traces are filterable by program, framework, run, - * and build type for cost attribution and dashboards. `skill_id` is omitted when - * the run has none. - */ -export function buildRunTags(args: { - programId: string; - integration: string; - runId: string; - build: string; - skillId?: string; -}): Record { - return { - program_id: args.programId, - integration: args.integration, - run_id: args.runId, - build: args.build, - // Triage and detection spread these tags and override this one. - call_type: CallType.agent, - ...(args.skillId ? { skill_id: args.skillId } : {}), - }; -} diff --git a/src/shared/scan-consent.ts b/src/shared/scan-consent.ts deleted file mode 100644 index 9188cac6c..000000000 --- a/src/shared/scan-consent.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** Features discovered by the feature-discovery subagent */ -export enum DiscoveredFeature { - Stripe = 'stripe', - LLM = 'llm', -} - -type ScanConsentState = { scanConsent: string }; - -/** One place to ask, so a new consent state does not need three edits. */ -export function mayReportScanResults(session: ScanConsentState): boolean { - return session.scanConsent === 'granted'; -} - -/** Lives here so analytics infrastructure never learns what consent means. */ -export function reportableDiscoveredFeatures( - session: ScanConsentState & { discoveredFeatures: DiscoveredFeature[] }, -): DiscoveredFeature[] | undefined { - return mayReportScanResults(session) ? session.discoveredFeatures : undefined; -} - -/** Also a scan result, so it travels under the same consent as the rest. */ -export function reportablePosthogSdkDetected( - session: ScanConsentState & { posthogSdkDetected: boolean }, -): boolean | undefined { - return mayReportScanResults(session) ? session.posthogSdkDetected : undefined; -} diff --git a/src/shared/self-driving-pricing.ts b/src/shared/self-driving-pricing.ts deleted file mode 100644 index 001c0f20f..000000000 --- a/src/shared/self-driving-pricing.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Self-driving pricing, in one place. It's stated at six points across the run (intro, tips, - * learn deck, outro), and a price quoted six ways is a price the reader stops trusting. - * - * Careful with the word "free". The tracking this wizard installs bills as ordinary PostHog - * usage, and scouts are heading for usage-based pricing too — so anything called free here is - * a promise with an expiry date. State what the PR costs and leave the rest to usage-based - * billing, which stays true either side of that change. - */ - -/** Flat USD charge per report that ships a pull request. */ -export const PRICE_PER_PR_USD = 15; - -/** One line, for a screen that only has room to say what it costs. */ -export const PRICING_SHORT = `Agents charge a flat $${PRICE_PER_PR_USD} per pull request they ship.`; - -/** The same fact at length, plus the bit people are surprised by later. */ -export const PRICING_LONG = - `A report that ships a pull request costs a flat $${PRICE_PER_PR_USD}. ` + - `Everything else follows your usual PostHog usage-based pricing.`; - -/** - * Nothing caps the spend unless you set it, which is worth saying out loud. Checked against - * billing: `custom_limits_map` starts empty and a product with no custom limit resolves to - * `usage_limit: None`, i.e. uncapped. A free plan is capped by its own allocation instead, - * hence the qualifier. - */ -export const NO_DEFAULT_LIMIT = - 'A paid plan has no monthly cap until you set one. Add a PR limit in your inbox sidebar, ' + - 'and agents pause when they hit it.'; diff --git a/src/shared/utils/cleanup-registry.ts b/src/shared/utils/cleanup-registry.ts deleted file mode 100644 index 071955574..000000000 --- a/src/shared/utils/cleanup-registry.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** Process-local cleanup callbacks shared by the CLI and agent. */ -const cleanupFns: Array<() => void> = []; - -export function registerCleanup(fn: () => void): void { - cleanupFns.push(fn); -} - -export function clearCleanup(): void { - cleanupFns.length = 0; -} - -/** Runs all registered cleanup functions and drains the array. */ -export function runCleanups(): void { - const fns = cleanupFns.splice(0); - for (const fn of fns) { - try { - fn(); - } catch { - /* cleanup should not prevent exit */ - } - } -} diff --git a/src/shared/utils/oauth-token.ts b/src/shared/utils/oauth-token.ts deleted file mode 100644 index 92443d5c5..000000000 --- a/src/shared/utils/oauth-token.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** OAuth token-endpoint helpers that need no UI: the token response and the refresh grant. */ -import axios from 'axios'; -import { z } from 'zod'; -import { - POSTHOG_DEV_CLIENT_ID, - POSTHOG_PROXY_CLIENT_ID, - WIZARD_USER_AGENT, -} from '@shared/constants'; -import { logToFile } from './debug'; -import { getOAuthUrl, resolveBaseUrl } from './urls'; -import { oauthErrorFromTokenBody } from './oauth-errors'; - -export const OAuthTokenResponseSchema = z.object({ - access_token: z.string(), - expires_in: z.number(), - token_type: z.string(), - scope: z.string(), - refresh_token: z.string().optional(), - scoped_teams: z.array(z.number()).optional(), - scoped_organizations: z.array(z.string()).optional(), - // Sent by PostHog Cloud (and passed through the oauth.posthog.com proxy); absent on - // self-hosted. `.catch(undefined)` so an unrecognized value degrades to the probe - // fallback instead of failing the whole login. - posthog_region: z.enum(['us', 'eu']).optional().catch(undefined), - posthog_base_url: z.string().optional().catch(undefined), -}); - -export type OAuthTokenResponse = z.infer; - -/** - * OAuth client ID for the current target. A pinned base URL (`--base-url`, or - * IS_DEV's implicit localhost) means we're talking to a dev-seeded stack, which - * registers the dev client; prod uses the proxy client. - * - * TODO: this assumes any pinned base URL is a dev-seeded instance that - * registers POSTHOG_DEV_CLIENT_ID. If we ever point `--base-url` at a non-dev - * instance with its own OAuth app, make the client ID configurable (e.g. a - * `--oauth-client-id` flag) instead of always falling back to the dev client. - */ -export function getOAuthClientId(baseUrl?: string): string { - return resolveBaseUrl(baseUrl) - ? POSTHOG_DEV_CLIENT_ID - : POSTHOG_PROXY_CLIENT_ID; -} - -// Refresh-token grant (RFC 6749 §6); the server rotates, so callers must store the returned refresh_token. -export async function refreshAccessToken( - refreshToken: string, - baseUrl?: string, - clientId?: string, -): Promise { - const oauthUrl = getOAuthUrl(baseUrl); - logToFile(`[oauth] refreshing access token at ${oauthUrl}/oauth/token`); - try { - const response = await axios.post( - `${oauthUrl}/oauth/token`, - { - grant_type: 'refresh_token', - refresh_token: refreshToken, - // The grant only refreshes under its minting app — provisioning signups pass their regional client. - client_id: clientId ?? getOAuthClientId(baseUrl), - }, - { - headers: { - 'Content-Type': 'application/json', - 'User-Agent': WIZARD_USER_AGENT, - }, - timeout: 30_000, - }, - ); - const token = OAuthTokenResponseSchema.parse(response.data); - logToFile('[oauth] access token refreshed'); - return token; - } catch (e) { - logToFile( - '[oauth] token refresh failed:', - e instanceof Error ? e.message : e, - ); - const refreshError = axios.isAxiosError(e) - ? oauthErrorFromTokenBody(e.response?.data) - : null; - throw refreshError ?? e; - } -} diff --git a/src/shared/utils/oauth.ts b/src/shared/utils/oauth.ts index 5ac3f5934..dda07db81 100644 --- a/src/shared/utils/oauth.ts +++ b/src/shared/utils/oauth.ts @@ -3,10 +3,13 @@ import * as http from 'node:http'; import { execSync } from 'node:child_process'; import axios from 'axios'; import { logToFile } from './debug'; +import { z } from 'zod'; import { getUI } from '@ui'; import { OAUTH_PORTS, OAUTH_TIMEOUT_MS, + POSTHOG_DEV_CLIENT_ID, + POSTHOG_PROXY_CLIENT_ID, WIZARD_USER_AGENT, } from '@shared/constants'; import { getOAuthUrl, resolveBaseUrl } from './urls'; @@ -20,17 +23,6 @@ import { oauthErrorFromCallbackParams, oauthErrorFromTokenBody, } from './oauth-errors'; -import { - getOAuthClientId, - OAuthTokenResponseSchema, - type OAuthTokenResponse, -} from './oauth-token'; - -export { - OAuthTokenResponseSchema, - refreshAccessToken, - type OAuthTokenResponse, -} from './oauth-token'; const OAUTH_CALLBACK_STYLES = ` `; +export const OAuthTokenResponseSchema = z.object({ + access_token: z.string(), + expires_in: z.number(), + token_type: z.string(), + scope: z.string(), + refresh_token: z.string().optional(), + scoped_teams: z.array(z.number()).optional(), + scoped_organizations: z.array(z.string()).optional(), + // Sent by PostHog Cloud (and passed through the oauth.posthog.com proxy); absent on + // self-hosted. `.catch(undefined)` so an unrecognized value degrades to the probe + // fallback instead of failing the whole login. + posthog_region: z.enum(['us', 'eu']).optional().catch(undefined), + posthog_base_url: z.string().optional().catch(undefined), +}); + +export type OAuthTokenResponse = z.infer; + export const WIZARD_COMPLETION_SCOPE = 'event_definition:write'; export function parseOAuthScopes(scope: string): string[] { @@ -108,6 +117,22 @@ interface OAuthConfig { baseUrl?: string; } +/** + * OAuth client ID for the current target. A pinned base URL (`--base-url`, or + * IS_DEV's implicit localhost) means we're talking to a dev-seeded stack, which + * registers the dev client; prod uses the proxy client. + * + * TODO: this assumes any pinned base URL is a dev-seeded instance that + * registers POSTHOG_DEV_CLIENT_ID. If we ever point `--base-url` at a non-dev + * instance with its own OAuth app, make the client ID configurable (e.g. a + * `--oauth-client-id` flag) instead of always falling back to the dev client. + */ +function getOAuthClientId(baseUrl?: string): string { + return resolveBaseUrl(baseUrl) + ? POSTHOG_DEV_CLIENT_ID + : POSTHOG_PROXY_CLIENT_ID; +} + function getLocalOAuthOrigin(port: number): string { return `http://localhost:${port}`; } @@ -442,6 +467,46 @@ async function exchangeCodeForToken( return token; } +// Refresh-token grant (RFC 6749 §6); the server rotates, so callers must store the returned refresh_token. +export async function refreshAccessToken( + refreshToken: string, + baseUrl?: string, + clientId?: string, +): Promise { + const oauthUrl = getOAuthUrl(baseUrl); + logToFile(`[oauth] refreshing access token at ${oauthUrl}/oauth/token`); + try { + const response = await axios.post( + `${oauthUrl}/oauth/token`, + { + grant_type: 'refresh_token', + refresh_token: refreshToken, + // The grant only refreshes under its minting app — provisioning signups pass their regional client. + client_id: clientId ?? getOAuthClientId(baseUrl), + }, + { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': WIZARD_USER_AGENT, + }, + timeout: 30_000, + }, + ); + const token = OAuthTokenResponseSchema.parse(response.data); + logToFile('[oauth] access token refreshed'); + return token; + } catch (e) { + logToFile( + '[oauth] token refresh failed:', + e instanceof Error ? e.message : e, + ); + const refreshError = axios.isAxiosError(e) + ? oauthErrorFromTokenBody(e.response?.data) + : null; + throw refreshError ?? e; + } +} + /** * Warn — at login, while the user is still watching — when the grant came back * narrower than the request, and record the gap so narrowed runs are countable. diff --git a/src/shared/utils/wizard-abort.ts b/src/shared/utils/wizard-abort.ts index ec5275ac8..0a2b8ae30 100644 --- a/src/shared/utils/wizard-abort.ts +++ b/src/shared/utils/wizard-abort.ts @@ -17,9 +17,6 @@ import { emitWizardError, sanitizeErrorDetail, } from '@shared/errors'; -import { runCleanups } from './cleanup-registry'; - -export { registerCleanup, clearCleanup, runCleanups } from './cleanup-registry'; // Still importable from here; the class lives with the error codes. export { WizardError }; @@ -36,6 +33,28 @@ interface WizardAbortOptions { status?: 'error' | 'cancelled'; } +const cleanupFns: Array<() => void> = []; + +export function registerCleanup(fn: () => void): void { + cleanupFns.push(fn); +} + +export function clearCleanup(): void { + cleanupFns.length = 0; +} + +/** Runs all registered cleanup functions and drains the array. */ +export function runCleanups(): void { + const fns = cleanupFns.splice(0); + for (const fn of fns) { + try { + fn(); + } catch { + /* cleanup should not prevent exit */ + } + } +} + function resolveErrorCode( options: WizardAbortOptions, error: Error | WizardError | undefined, diff --git a/src/ui/tui/decks/self-driving/pricing.ts b/src/ui/tui/decks/self-driving/pricing.ts index 237c09860..001c0f20f 100644 --- a/src/ui/tui/decks/self-driving/pricing.ts +++ b/src/ui/tui/decks/self-driving/pricing.ts @@ -1,7 +1,30 @@ -/** UI compatibility entry for the self-driving pricing copy. */ -export { - PRICE_PER_PR_USD, - PRICING_SHORT, - PRICING_LONG, - NO_DEFAULT_LIMIT, -} from '@shared/self-driving-pricing'; +/** + * Self-driving pricing, in one place. It's stated at six points across the run (intro, tips, + * learn deck, outro), and a price quoted six ways is a price the reader stops trusting. + * + * Careful with the word "free". The tracking this wizard installs bills as ordinary PostHog + * usage, and scouts are heading for usage-based pricing too — so anything called free here is + * a promise with an expiry date. State what the PR costs and leave the rest to usage-based + * billing, which stays true either side of that change. + */ + +/** Flat USD charge per report that ships a pull request. */ +export const PRICE_PER_PR_USD = 15; + +/** One line, for a screen that only has room to say what it costs. */ +export const PRICING_SHORT = `Agents charge a flat $${PRICE_PER_PR_USD} per pull request they ship.`; + +/** The same fact at length, plus the bit people are surprised by later. */ +export const PRICING_LONG = + `A report that ships a pull request costs a flat $${PRICE_PER_PR_USD}. ` + + `Everything else follows your usual PostHog usage-based pricing.`; + +/** + * Nothing caps the spend unless you set it, which is worth saying out loud. Checked against + * billing: `custom_limits_map` starts empty and a product with no custom limit resolves to + * `usage_limit: None`, i.e. uncapped. A free plan is capped by its own allocation instead, + * hence the qualifier. + */ +export const NO_DEFAULT_LIMIT = + 'A paid plan has no monthly cap until you set one. Add a PR limit in your inbox sidebar, ' + + 'and agents pause when they hit it.'; diff --git a/src/ui/tui/hooks/file-watcher.ts b/src/ui/tui/hooks/file-watcher.ts index 974466508..5dd272aa0 100644 --- a/src/ui/tui/hooks/file-watcher.ts +++ b/src/ui/tui/hooks/file-watcher.ts @@ -1,14 +1,8 @@ import { useEffect } from 'react'; -import { - startFileWatcher, - type FileWatcherOptions, -} from '@shared/file-watcher'; +import { startFileWatcher, type FileWatcherOptions } from '@lib/file-watcher'; -export { startFileWatcher } from '@shared/file-watcher'; -export type { - FileWatcherHandle, - FileWatcherOptions, -} from '@shared/file-watcher'; +export { startFileWatcher } from '@lib/file-watcher'; +export type { FileWatcherHandle, FileWatcherOptions } from '@lib/file-watcher'; /** React hook wrapping `startFileWatcher`. Starts on mount, stops on unmount * or when `path` changes. `onUpdate` and `options` are captured at mount From 7360d300442b1fa072150fe10e8a51f5f670c6be Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 21:01:55 -0400 Subject: [PATCH 59/61] test(architecture): name the callers, not hosts Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- src/__tests__/architecture/import-boundaries.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index e4b566abd..ac55e216a 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -483,7 +483,7 @@ describe('agent entry modules', () => { describe('programs entry modules', () => { const rule = (from: string, to: string) => ruleFor(from, to); - it('lets hosts reach programs through its entries', () => { + it('lets the CLI and TUI reach programs through its entries', () => { expect(rule('src/commands/audit.ts', 'src/programs/index.ts')).toBe(null); expect(rule('src/ui/tui/store.ts', 'src/programs/types.ts')).toBe(null); expect(rule('src/commands/audit.ts', 'src/programs/audit/index.ts')).toBe( From 7a1404710b21be78d758e55df90d889350a5df29 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 25 Sep 2026 10:57:01 -0400 Subject: [PATCH 60/61] refactor(programs): keep B1 to moves: bindings notes and the detection test The moved detection test matches main again, and the bindings notes name no release. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .eslintrc.cjs | 4 +- .../architecture/import-boundaries.test.ts | 2 +- src/agent/index.ts | 2 +- src/agent/types.ts | 2 +- .../__tests__/agentic-progress.test.ts | 77 ++++--------------- 5 files changed, 22 insertions(+), 65 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index cfb73fabc..a60c79a0a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -35,7 +35,7 @@ module.exports = { // progress events and asks through an injected answerer, so nothing // here may import a UI, the session, detection, the CLI or a program. // Only direct static imports are checked. ProgramId remains a type-only - // exception until B2 moves PROGRAM_BINDINGS to programs. + // exception until PROGRAM_BINDINGS moves to programs. files: ['src/agent/**/*.ts'], excludedFiles: ['**/__tests__/**'], rules: { @@ -90,7 +90,7 @@ module.exports = { group: ['@programs', '@programs/**', '**/programs/**'], allowTypeImports: true, message: - 'The agent takes program data through RunConfig. ProgramId types remain until B2 moves PROGRAM_BINDINGS to programs.', + 'The agent takes program data through RunConfig. ProgramId types remain until PROGRAM_BINDINGS moves to programs.', }, ], }, diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts index ac55e216a..64874af0c 100644 --- a/src/__tests__/architecture/import-boundaries.test.ts +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -48,7 +48,7 @@ export const ALLOWED_IMPORTS: Record = { env: [], shared: ['env', 'shared'], legacy: ['env', 'shared', 'legacy', 'programs'], - // B2 moves bindings and removes the agent's remaining ProgramId type imports. + // Moving the bindings to programs removes the agent's ProgramId type imports. agent: ['env', 'shared', 'agent'], programs: ['env', 'shared', 'agent', 'programs'], tui: ['env', 'shared', 'legacy', 'programs', 'tui'], diff --git a/src/agent/index.ts b/src/agent/index.ts index 605eeb0c8..3d5c77d46 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -19,7 +19,7 @@ export { AgentSignals } from './agent-interface'; export { WIZARD_TOOL_NAMES } from './tools'; /** - * Leaves in B2 (B1 deferred the bindings). Bindings and program data move to + * Leaves when the bindings move to programs. Bindings and program data move to * programs: resolveBinding * is keyed by PROGRAM_BINDINGS and the agent keeps only "run from an * already-resolved binding"; shouldDisableAsk is a flags policy programs diff --git a/src/agent/types.ts b/src/agent/types.ts index e3476645b..b23200cfb 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -28,7 +28,7 @@ export type { TokenUsageDelta, } from './progress'; -/** Leaves in B2 with the bindings table; B1 deferred PROGRAM_BINDINGS. */ +/** Leaves when the bindings table moves to programs. */ export type { ProgramBinding, SwitchboardCtx } from './runner'; /** Leaves in B2 with downloadSkill. */ diff --git a/src/programs/detection/__tests__/agentic-progress.test.ts b/src/programs/detection/__tests__/agentic-progress.test.ts index 4839df7aa..424c98414 100644 --- a/src/programs/detection/__tests__/agentic-progress.test.ts +++ b/src/programs/detection/__tests__/agentic-progress.test.ts @@ -1,13 +1,12 @@ import { detectProjectsWithAgent } from '../agentic'; import { - AgentErrorType, initializeAgent, runAgent as executeAgent, } from '@agent/agent-interface'; import { buildSession } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; -import { ErrorCodes } from '@shared/errors'; import { getUI } from '@ui'; +import { ErrorCodes } from '@shared/errors'; vi.mock('@utils/debug'); vi.mock('@ui', () => ({ getUI: () => ui })); @@ -24,22 +23,6 @@ vi.mock('@agent/agent-interface', async (original) => ({ runAgent: vi.fn(), })); -function detectionSession() { - const session = buildSession({ installDir: '/tmp/detection-test' }); - session.credentials = { - accessToken: 'test', - projectApiKey: 'phc_test', - projectId: 1, - host: HostResolution.fromApiHost('https://us.posthog.com'), - }; - return session; -} - -beforeEach(() => { - vi.mocked(initializeAgent).mockReset(); - vi.mocked(executeAgent).mockReset(); -}); - it('keeps initialization and execution progress visible during detection', async () => { const delta = { inputTokens: 5, @@ -77,7 +60,14 @@ it('keeps initialization and execution progress visible during detection', async return Promise.resolve({ kind: 'success' }); }, ); - const report = await detectProjectsWithAgent(detectionSession(), { + const session = buildSession({ installDir: '/tmp/detection-test' }); + session.credentials = { + accessToken: 'test', + projectApiKey: 'phc_test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }; + const report = await detectProjectsWithAgent(session, { programId: 'posthog-integration', targets: [{ id: 'node', name: 'Node.js' }], }); @@ -110,51 +100,18 @@ it('stops optional detection on a data-only 401 before parsing partial JSON', as }); }, ); + const session = buildSession({ installDir: '/tmp/detection-test' }); + session.credentials = { + accessToken: 'test', + projectApiKey: 'phc_test', + projectId: 1, + host: HostResolution.fromApiHost('https://us.posthog.com'), + }; await expect( - detectProjectsWithAgent(detectionSession(), { + detectProjectsWithAgent(session, { programId: 'posthog-integration', targets: [{ id: 'node', name: 'Node.js' }], }), ).rejects.toThrow('Authentication failed (401)'); expect(ui.showAuthError).not.toHaveBeenCalled(); }); - -it('preserves the original error from a decided failure', async () => { - const original = new Error('Gateway bearer rejected'); - vi.mocked(initializeAgent).mockResolvedValue( - {} as Awaited>, - ); - vi.mocked(executeAgent).mockResolvedValue({ - kind: 'decided_failure', - failure: { - code: ErrorCodes.GatewayMintRefused, - message: original.message, - error: original, - }, - }); - - await expect( - detectProjectsWithAgent(detectionSession(), { - programId: 'posthog-integration', - targets: [{ id: 'node', name: 'Node.js' }], - }), - ).rejects.toBe(original); -}); - -it('rejects classified agent failures', async () => { - vi.mocked(initializeAgent).mockResolvedValue( - {} as Awaited>, - ); - vi.mocked(executeAgent).mockResolvedValue({ - kind: 'failure', - classification: AgentErrorType.API_ERROR, - message: 'Agent API unavailable', - }); - - await expect( - detectProjectsWithAgent(detectionSession(), { - programId: 'posthog-integration', - targets: [{ id: 'node', name: 'Node.js' }], - }), - ).rejects.toThrow('Agent API unavailable'); -}); From 3991926bfc3aae07ee3f06143dfcafd230b62e12 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 25 Sep 2026 12:38:55 -0400 Subject: [PATCH 61/61] refactor(programs): move FRAMEWORK_REGISTRY into frameworks/ The registry only maps integrations to framework configs, so it lives beside them. Importers now use @programs/frameworks/registry. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .claude/skills/adding-framework-support/SKILL.md | 2 +- e2e-harness/__tests__/e2e-flow-snapshot.test.ts | 2 +- e2e-harness/__tests__/wizard-ci-driver.test.ts | 2 +- scripts/tui-host.no-jest.ts | 2 +- src/__tests__/architecture/known-violations.json | 6 +++--- src/__tests__/cli.test.ts | 2 +- src/__tests__/provision-cli.test.ts | 2 +- src/programs/__tests__/flow-traces.test.ts | 2 +- src/programs/detection/framework.ts | 2 +- src/programs/detection/project-scope.ts | 2 +- src/programs/error-tracking/index.ts | 2 +- src/programs/{ => frameworks}/registry.ts | 2 +- src/programs/posthog-integration/detect.ts | 2 +- src/programs/posthog-integration/index.ts | 2 +- src/programs/replay-vision/index.ts | 2 +- src/programs/run-agent-legacy.ts | 2 +- src/ui/tui/__tests__/frames.test.tsx | 2 +- src/ui/tui/__tests__/keyboard-equivalence.test.tsx | 2 +- src/ui/tui/__tests__/router.test.ts | 2 +- src/ui/tui/__tests__/store-invariants.test.ts | 2 +- src/ui/tui/screens/ErrorTrackingDetectScreen.tsx | 2 +- .../tui/screens/PostHogIntegrationIntroScreen.tsx | 14 ++++++++------ .../screens/SelfDrivingIntegrationDetectScreen.tsx | 2 +- 23 files changed, 32 insertions(+), 30 deletions(-) rename src/programs/{ => frameworks}/registry.ts (98%) diff --git a/.claude/skills/adding-framework-support/SKILL.md b/.claude/skills/adding-framework-support/SKILL.md index 625e7c562..903ef23f0 100644 --- a/.claude/skills/adding-framework-support/SKILL.md +++ b/.claude/skills/adding-framework-support/SKILL.md @@ -33,7 +33,7 @@ Integration instructions and examples belong in context-mill. 2. Add the config under `src/programs/frameworks//-wizard-agent.ts`. Use a `type` for framework context so it satisfies `Record`. Export the config; the integration program already supplies execution. -3. Import the config into [FRAMEWORK_REGISTRY](../../../src/programs/registry.ts). +3. Import the config into [FRAMEWORK_REGISTRY](../../../src/programs/frameworks/registry.ts). The display label comes from `metadata.name`. Read the current interface for the complete required fields. In particular, diff --git a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts index ac9d7b85c..ab0c24fcc 100644 --- a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts +++ b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts @@ -19,7 +19,7 @@ import { setUI } from '@ui/index'; import { buildSession, RunPhase } from '@lib/wizard-session'; import { Integration } from '@shared/constants'; import { HostResolution } from '@shared/host-resolution'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { Program, diff --git a/e2e-harness/__tests__/wizard-ci-driver.test.ts b/e2e-harness/__tests__/wizard-ci-driver.test.ts index a634ff9c5..474cacbb6 100644 --- a/e2e-harness/__tests__/wizard-ci-driver.test.ts +++ b/e2e-harness/__tests__/wizard-ci-driver.test.ts @@ -15,7 +15,7 @@ import { setUI } from '@ui/index'; import { buildSession, RunPhase, McpOutcome } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { ScreenId, Overlay } from '@ui/tui/router'; import { Program } from '@programs'; diff --git a/scripts/tui-host.no-jest.ts b/scripts/tui-host.no-jest.ts index c34f718eb..2d61ec23e 100644 --- a/scripts/tui-host.no-jest.ts +++ b/scripts/tui-host.no-jest.ts @@ -35,7 +35,7 @@ import { getOrAskForProjectData } from '@utils/setup-utils'; import { logToFile } from '@utils/debug'; import { join } from 'path'; import { detectFramework } from '@programs/detection/index'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import type { Integration } from '@shared/constants'; import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; import { ERROR_TRACKING_PROJECT_PATH_KEY } from '@programs/error-tracking/detect-agentic'; diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json index 0d110b9d3..35d757ee0 100644 --- a/src/__tests__/architecture/known-violations.json +++ b/src/__tests__/architecture/known-violations.json @@ -138,13 +138,13 @@ "src/ui/tui/screen-sequences.ts -> src/programs/ai-opt-in-gate.ts", "src/ui/tui/screen-sequences.ts -> src/programs/program-step.ts", "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx -> src/programs/error-tracking/detect-agentic.ts", - "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx -> src/programs/registry.ts", - "src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx -> src/programs/registry.ts", + "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx -> src/programs/frameworks/registry.ts", + "src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx -> src/programs/frameworks/registry.ts", "src/ui/tui/screens/RevenueIntroScreen.tsx -> src/programs/revenue-analytics/index.ts", "src/ui/tui/screens/SelfDrivingGitHubScreen.tsx -> src/programs/self-driving/detect.ts", "src/ui/tui/screens/SelfDrivingHandoffScreen.tsx -> src/programs/posthog-integration/index.ts", "src/ui/tui/screens/SelfDrivingHandoffScreen.tsx -> src/programs/self-driving/detect.ts", - "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/registry.ts", + "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/frameworks/registry.ts", "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/self-driving/detect-agentic.ts", "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx -> src/programs/self-driving/detect.ts", "src/ui/tui/screens/SelfDrivingIntroScreen.tsx -> src/programs/self-driving/index.ts", diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts index c740d3fc8..ebf38ccbe 100644 --- a/src/__tests__/cli.test.ts +++ b/src/__tests__/cli.test.ts @@ -101,7 +101,7 @@ vi.mock('@utils/debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn(), })); -vi.mock('../programs/registry', () => ({ FRAMEWORK_REGISTRY: {} })); +vi.mock('../programs/frameworks/registry', () => ({ FRAMEWORK_REGISTRY: {} })); vi.mock('../programs/detection', () => ({ detectFramework: vi.fn().mockResolvedValue(null), gatherFrameworkContext: vi.fn().mockResolvedValue({}), diff --git a/src/__tests__/provision-cli.test.ts b/src/__tests__/provision-cli.test.ts index 0224d25fa..1f3429d5e 100644 --- a/src/__tests__/provision-cli.test.ts +++ b/src/__tests__/provision-cli.test.ts @@ -52,7 +52,7 @@ vi.mock('@utils/debug', () => ({ logToFile: vi.fn(), setDebugSink: vi.fn(), })); -vi.mock('../programs/registry', () => ({ FRAMEWORK_REGISTRY: {} })); +vi.mock('../programs/frameworks/registry', () => ({ FRAMEWORK_REGISTRY: {} })); vi.mock('../programs/detection', () => ({ detectFramework: vi.fn().mockResolvedValue(null), gatherFrameworkContext: vi.fn().mockResolvedValue({}), diff --git a/src/programs/__tests__/flow-traces.test.ts b/src/programs/__tests__/flow-traces.test.ts index 30f7ec314..f48428f3c 100644 --- a/src/programs/__tests__/flow-traces.test.ts +++ b/src/programs/__tests__/flow-traces.test.ts @@ -19,7 +19,7 @@ import { type WizardSession, } from '@lib/wizard-session'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { HostResolution } from '@shared/host-resolution'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { analytics } from '@utils/analytics'; diff --git a/src/programs/detection/framework.ts b/src/programs/detection/framework.ts index 01b4f2105..a06ddd0dd 100644 --- a/src/programs/detection/framework.ts +++ b/src/programs/detection/framework.ts @@ -7,7 +7,7 @@ */ import { Integration, DETECTION_TIMEOUT_MS } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; /** * Loop through all registered frameworks and return the first one diff --git a/src/programs/detection/project-scope.ts b/src/programs/detection/project-scope.ts index 916a87b52..3842944a9 100644 --- a/src/programs/detection/project-scope.ts +++ b/src/programs/detection/project-scope.ts @@ -10,7 +10,7 @@ import { type DetectTarget, } from './agentic.js'; import { authenticate } from '@programs/authenticate'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { Integration, WIZARD_BASIC_INTEGRATION_AGENTIC_DETECTION_FLAG_KEY, diff --git a/src/programs/error-tracking/index.ts b/src/programs/error-tracking/index.ts index 12924b116..ebfc4db78 100644 --- a/src/programs/error-tracking/index.ts +++ b/src/programs/error-tracking/index.ts @@ -1,7 +1,7 @@ import { Integration } from '@shared/constants'; import { detectFramework } from '@programs/detection/index'; import { scopeInstallDirToProject } from '@programs/detection/project-scope'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import type { ProgramRun } from '@programs/program-run'; import { AGENT_SKILL_STEPS } from '@programs/agent-skill/steps'; import { getContentBlocks } from '@ui/tui/decks/error-tracking/index'; diff --git a/src/programs/registry.ts b/src/programs/frameworks/registry.ts similarity index 98% rename from src/programs/registry.ts rename to src/programs/frameworks/registry.ts index 649dc3a99..4c2a79a70 100644 --- a/src/programs/registry.ts +++ b/src/programs/frameworks/registry.ts @@ -1,4 +1,4 @@ -import type { FrameworkConfig } from './framework-config'; +import type { FrameworkConfig } from '@programs/framework-config'; import { Integration } from '@shared/constants'; import { NEXTJS_AGENT_CONFIG } from '@programs/frameworks/nextjs/nextjs-wizard-agent'; import { NUXT_AGENT_CONFIG } from '@programs/frameworks/nuxt/nuxt-wizard-agent'; diff --git a/src/programs/posthog-integration/detect.ts b/src/programs/posthog-integration/detect.ts index ab4effb72..09be0515d 100644 --- a/src/programs/posthog-integration/detect.ts +++ b/src/programs/posthog-integration/detect.ts @@ -16,7 +16,7 @@ import { ScanConsent, type WizardSession, } from '@lib/wizard-session'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { detectFramework, discoverFeatures, diff --git a/src/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts index a5ed7b73b..03f63624c 100644 --- a/src/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -15,7 +15,7 @@ import { gatherFrameworkContext, } from '@programs/detection/index'; import { scopeInstallDirToProject } from '@programs/detection/project-scope'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; import { diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index 314bfc9a4..c266f6f51 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -5,7 +5,7 @@ import { gatherFrameworkContext, } from '@programs/detection/index'; import { scopeInstallDirToProject } from '@programs/detection/project-scope'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { createSkillProgram } from '@programs/agent-skill/index'; import { AGENT_SKILL_STEPS } from '@programs/agent-skill/steps'; import { detectPostHogIntegration } from '@programs/posthog-integration/detect'; diff --git a/src/programs/run-agent-legacy.ts b/src/programs/run-agent-legacy.ts index 3eb00d72c..ee4134fd9 100644 --- a/src/programs/run-agent-legacy.ts +++ b/src/programs/run-agent-legacy.ts @@ -57,7 +57,7 @@ import { WIZARD_SELF_DRIVING_USE_PI_HARNESS_FLAG_KEY, type Integration, } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { postAuthGateSteps, type ProgramConfig } from './program-step'; import { authenticate, refreshAccessTokenIfNeeded } from './authenticate'; import { maybeStampAiSdkDetected } from './posthog-integration/detect'; diff --git a/src/ui/tui/__tests__/frames.test.tsx b/src/ui/tui/__tests__/frames.test.tsx index 935331e94..f8700cf03 100644 --- a/src/ui/tui/__tests__/frames.test.tsx +++ b/src/ui/tui/__tests__/frames.test.tsx @@ -113,7 +113,7 @@ import { } from '@lib/wizard-session'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import type { FrameworkConfig } from '@programs/types'; import { Program, type ProgramId } from '@programs'; import { diff --git a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx index 43e9d24cb..b23e649bc 100644 --- a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx +++ b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx @@ -25,7 +25,7 @@ import { type WizardSession, } from '@lib/wizard-session'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { HostResolution } from '@shared/host-resolution'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { SOURCE_MAPS_CONTEXT_KEYS } from '@programs/error-tracking-upload-source-maps/detect'; diff --git a/src/ui/tui/__tests__/router.test.ts b/src/ui/tui/__tests__/router.test.ts index 2f20c0608..62762bab8 100644 --- a/src/ui/tui/__tests__/router.test.ts +++ b/src/ui/tui/__tests__/router.test.ts @@ -8,7 +8,7 @@ import { HostResolution } from '@shared/host-resolution'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { WizardRouter, ScreenId, Overlay, Program } from '@ui/tui/router'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { PROGRAM_REGISTRY } from '@programs'; function baseWizardSession() { diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts index 2496bdc4c..c002c46e6 100644 --- a/src/ui/tui/__tests__/store-invariants.test.ts +++ b/src/ui/tui/__tests__/store-invariants.test.ts @@ -28,7 +28,7 @@ import { PROGRAM_SEQUENCES } from '@ui/tui/screen-sequences'; import { WizardReadiness } from '@shared/health-checks/readiness'; import { HostResolution } from '@shared/host-resolution'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { analytics } from '@utils/analytics'; import { PROGRAM_REGISTRY } from '@programs'; import type { SettingsConflict } from '@shared/claude-settings'; diff --git a/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx b/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx index 4cf416e1d..4d2c3eaa9 100644 --- a/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx +++ b/src/ui/tui/screens/ErrorTrackingDetectScreen.tsx @@ -9,7 +9,7 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import type { WizardStore } from '@ui/tui/store'; import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { detectErrorTrackingProjects, ERROR_TRACKING_PROJECT_PATH_KEY, diff --git a/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx b/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx index 09d933648..c65cca609 100644 --- a/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx +++ b/src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx @@ -102,12 +102,14 @@ const FrameworkPicker = ({ options={options} onSelect={(value) => { const integration = Array.isArray(value) ? value[0] : value; - void import('@programs/registry').then(({ FRAMEWORK_REGISTRY }) => { - const config = FRAMEWORK_REGISTRY[integration]; - store.setFrameworkConfig(integration, config); - store.setDetectedFramework(config.metadata.name); - onComplete?.(); - }); + void import('@programs/frameworks/registry').then( + ({ FRAMEWORK_REGISTRY }) => { + const config = FRAMEWORK_REGISTRY[integration]; + store.setFrameworkConfig(integration, config); + store.setDetectedFramework(config.metadata.name); + onComplete?.(); + }, + ); }} /> ); diff --git a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx index 6db94d501..7c42ccd5f 100644 --- a/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx +++ b/src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx @@ -15,7 +15,7 @@ import type { WizardStore } from '@ui/tui/store'; import { LoadingBox, PickerMenu } from '@ui/tui/primitives/index'; import { Colors, Icons } from '@ui/tui/styles'; import { Integration } from '@shared/constants'; -import { FRAMEWORK_REGISTRY } from '@programs/registry'; +import { FRAMEWORK_REGISTRY } from '@programs/frameworks/registry'; import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@programs/self-driving/detect'; import { detectSelfDrivingIntegrationProjects,