From 8a1f682ec3d2dffc31eab4980eb907bd6ecb67ba Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 11 May 2026 10:24:00 +0200 Subject: [PATCH] docs: add plan for managed runtime sl query integration --- ...aged-python-runtime-command-integration.md | 935 ++++++++++++++++++ 1 file changed, 935 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md diff --git a/docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md b/docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md new file mode 100644 index 00000000..733b6915 --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md @@ -0,0 +1,935 @@ +# Managed Python Runtime Command Integration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `ktx sl query` use the KTX-managed bundled Python runtime +instead of relying on a user-provided `python -m ktx_daemon`. + +**Architecture:** Add a small CLI helper that resolves the managed runtime, +installs the `core` feature when policy permits it, and creates the existing +`@ktx/context/daemon` one-shot semantic-layer compute port with the managed +`ktx-daemon` executable. Wire `ktx sl query` to pass an explicit runtime +install policy from `--yes`, `--no-input`, or the default interactive mode. + +**Tech Stack:** TypeScript, Commander, Vitest, `@clack/prompts`, +`@ktx/context/daemon`, existing KTX managed runtime installer. + +--- + +## Existing status + +This plan is based on +`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`. + +Existing plans based on the spec: + +- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md` is + implemented. The worktree contains + `scripts/build-python-runtime-wheel.mjs`, + `scripts/build-python-runtime-wheel.test.mjs`, runtime-wheel packaging in + `scripts/package-artifacts.mjs`, release-policy coverage, and matching + artifact tests. The targeted verification passes: + `node --test scripts/build-python-runtime-wheel.test.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs`. +- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md` is + implemented. The worktree contains + `packages/cli/src/managed-python-runtime.ts`, + `packages/cli/src/runtime.ts`, + `packages/cli/src/commands/runtime-commands.ts`, CLI registration, and + matching Vitest coverage. The targeted CLI verification passes: + `pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts src/runtime.test.ts src/index.test.ts`. + +Spec requirements still outside this plan: + +- `ktx runtime start` and `ktx runtime stop`. +- Managed HTTP daemon state, health checks, reuse, and stale daemon repair. +- Lazy `local-embeddings` installation and local embedding daemon reuse. +- Public npm package rename from `@ktx/cli` to `@kaelio/ktx`. + +This plan implements the next runnable user path: `ktx sl query` installs or +uses the managed `core` Python runtime according to the command's input policy. + +## File structure + +- Create `packages/cli/src/managed-python-command.ts`: CLI helper for managed + runtime policy, optional prompt, runtime install, and managed semantic-layer + compute port creation. +- Create `packages/cli/src/managed-python-command.test.ts`: unit tests for + ready runtime reuse, `--no-input` failure, `--yes` installation, and + interactive prompt acceptance. +- Modify `packages/cli/src/sl.ts`: extend `KtxSlArgs` with CLI version and + runtime install policy for `query`, and use the managed helper when no test + compute port is injected. +- Modify `packages/cli/src/sl.test.ts`: update existing `query` arguments and + assert `runKtxSl` delegates default compute creation to the managed helper. +- Modify `packages/cli/src/commands/sl-commands.ts`: add `--yes` and + `--no-input` to `sl query`, derive the runtime install policy, and pass the + CLI package version. +- Modify `packages/cli/src/command-schemas.ts`: validate `cliVersion` and + `runtimeInstallPolicy` on parsed `sl query` arguments. +- Modify `packages/cli/src/index.test.ts`: assert Commander routes the new + `sl query` runtime policy flags. + +### Task 1: Add failing managed Python command helper tests + +**Files:** + +- Create: `packages/cli/src/managed-python-command.test.ts` +- Test: `packages/cli/src/managed-python-command.test.ts` + +- [ ] **Step 1: Write the failing test file** + +Create `packages/cli/src/managed-python-command.test.ts` with this content: + +```typescript +import { describe, expect, it, vi } from 'vitest'; +import { + createManagedPythonSemanticLayerComputePort, + managedRuntimeInstallCommand, +} from './managed-python-command.js'; +import type { + InstalledKtxRuntimeManifest, + KtxRuntimeFeature, + ManagedPythonRuntimeInstallResult, + ManagedPythonRuntimeLayout, + ManagedPythonRuntimeStatus, +} from './managed-python-runtime.js'; + +function makeIo() { + let stdout = ''; + let stderr = ''; + return { + io: { + stdout: { + write: (chunk: string) => { + stdout += chunk; + }, + }, + stderr: { + write: (chunk: string) => { + stderr += chunk; + }, + }, + }, + stdout: () => stdout, + stderr: () => stderr, + }; +} + +function layout(): ManagedPythonRuntimeLayout { + return { + cliVersion: '0.2.0', + runtimeRoot: '/runtime', + versionDir: '/runtime/0.2.0', + venvDir: '/runtime/0.2.0/.venv', + manifestPath: '/runtime/0.2.0/manifest.json', + installLogPath: '/runtime/0.2.0/install.log', + assetDir: '/assets/python', + assetManifestPath: '/assets/python/manifest.json', + pythonPath: '/runtime/0.2.0/.venv/bin/python', + daemonPath: '/runtime/0.2.0/.venv/bin/ktx-daemon', + }; +} + +function manifest(features: KtxRuntimeFeature[] = ['core']): InstalledKtxRuntimeManifest { + return { + schemaVersion: 1, + cliVersion: '0.2.0', + installedAt: '2026-05-11T00:00:00.000Z', + asset: { + schemaVersion: 1, + distributionName: 'kaelio-ktx', + normalizedName: 'kaelio_ktx', + version: '0.2.0', + wheel: { + file: 'kaelio_ktx-0.2.0-py3-none-any.whl', + sha256: 'a'.repeat(64), + bytes: 123, + }, + }, + features, + python: { + executable: '/runtime/0.2.0/.venv/bin/python', + daemonExecutable: '/runtime/0.2.0/.venv/bin/ktx-daemon', + }, + installLog: '/runtime/0.2.0/install.log', + }; +} + +function readyStatus(features: KtxRuntimeFeature[] = ['core']): ManagedPythonRuntimeStatus { + return { + kind: 'ready', + detail: 'Runtime ready at /runtime/0.2.0', + layout: layout(), + manifest: manifest(features), + }; +} + +function missingStatus(): ManagedPythonRuntimeStatus { + return { + kind: 'missing', + detail: 'No runtime manifest at /runtime/0.2.0/manifest.json', + layout: layout(), + }; +} + +function installResult(features: KtxRuntimeFeature[] = ['core']): ManagedPythonRuntimeInstallResult { + const installedManifest = manifest(features); + return { + status: 'installed', + layout: layout(), + asset: { + manifest: installedManifest.asset, + wheelPath: '/assets/python/kaelio_ktx-0.2.0-py3-none-any.whl', + }, + manifest: installedManifest, + }; +} + +describe('managedRuntimeInstallCommand', () => { + it('prints the exact command for each managed runtime feature', () => { + expect(managedRuntimeInstallCommand('core')).toBe('ktx runtime install --yes'); + expect(managedRuntimeInstallCommand('local-embeddings')).toBe( + 'ktx runtime install --feature local-embeddings --yes', + ); + }); +}); + +describe('createManagedPythonSemanticLayerComputePort', () => { + it('uses the managed ktx-daemon executable when the runtime is ready', async () => { + const io = makeIo(); + const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() }; + const createPythonCompute = vi.fn(() => compute); + + await expect( + createManagedPythonSemanticLayerComputePort({ + cliVersion: '0.2.0', + installPolicy: 'never', + io: io.io, + readStatus: vi.fn(async () => readyStatus()), + installRuntime: vi.fn(), + createPythonCompute, + }), + ).resolves.toBe(compute); + + expect(createPythonCompute).toHaveBeenCalledWith({ + command: '/runtime/0.2.0/.venv/bin/ktx-daemon', + args: [], + }); + expect(io.stderr()).toBe(''); + }); + + it('fails with a preparation command when input is disabled and the runtime is missing', async () => { + const io = makeIo(); + const installRuntime = vi.fn(); + + await expect( + createManagedPythonSemanticLayerComputePort({ + cliVersion: '0.2.0', + installPolicy: 'never', + io: io.io, + readStatus: vi.fn(async () => missingStatus()), + installRuntime, + }), + ).rejects.toThrow('KTX Python runtime is required for this command. Run: ktx runtime install --yes'); + + expect(installRuntime).not.toHaveBeenCalled(); + }); + + it('installs the core runtime without prompting when policy is auto', async () => { + const io = makeIo(); + const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() }; + const createPythonCompute = vi.fn(() => compute); + const installRuntime = vi.fn(async () => installResult()); + + await expect( + createManagedPythonSemanticLayerComputePort({ + cliVersion: '0.2.0', + installPolicy: 'auto', + io: io.io, + readStatus: vi.fn(async () => missingStatus()), + installRuntime, + createPythonCompute, + }), + ).resolves.toBe(compute); + + expect(installRuntime).toHaveBeenCalledWith({ + cliVersion: '0.2.0', + features: ['core'], + force: false, + }); + expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv'); + expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0'); + }); + + it('prompts before installing when policy is prompt', async () => { + const io = makeIo(); + const confirmInstall = vi.fn(async () => true); + const installRuntime = vi.fn(async () => installResult()); + + await createManagedPythonSemanticLayerComputePort({ + cliVersion: '0.2.0', + installPolicy: 'prompt', + io: io.io, + readStatus: vi.fn(async () => missingStatus()), + installRuntime, + createPythonCompute: vi.fn(() => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() })), + confirmInstall, + }); + + expect(confirmInstall).toHaveBeenCalledWith( + 'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?', + ); + expect(installRuntime).toHaveBeenCalledWith({ + cliVersion: '0.2.0', + features: ['core'], + force: false, + }); + }); +}); +``` + +- [ ] **Step 2: Run the failing test** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/managed-python-command.test.ts +``` + +Expected: FAIL with an import error for +`./managed-python-command.js`. + +### Task 2: Implement the managed Python command helper + +**Files:** + +- Create: `packages/cli/src/managed-python-command.ts` +- Test: `packages/cli/src/managed-python-command.test.ts` + +- [ ] **Step 1: Create the helper** + +Create `packages/cli/src/managed-python-command.ts` with this content: + +```typescript +import { cancel, confirm, isCancel } from '@clack/prompts'; +import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon'; +import type { KtxCliIo } from './cli-runtime.js'; +import { + installManagedPythonRuntime, + readManagedPythonRuntimeStatus, + type InstalledKtxRuntimeManifest, + type KtxRuntimeFeature, + type ManagedPythonRuntimeInstallOptions, + type ManagedPythonRuntimeInstallResult, + type ManagedPythonRuntimeLayout, + type ManagedPythonRuntimeLayoutOptions, + type ManagedPythonRuntimeStatus, +} from './managed-python-runtime.js'; + +export type KtxManagedPythonInstallPolicy = 'prompt' | 'auto' | 'never'; + +export interface ManagedPythonCommandRuntime { + layout: ManagedPythonRuntimeLayout; + manifest: InstalledKtxRuntimeManifest; +} + +export interface ManagedPythonCommandDeps { + readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise; + installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise; + confirmInstall?: (message: string) => Promise; +} + +export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps { + cliVersion: string; + installPolicy: KtxManagedPythonInstallPolicy; + io: KtxCliIo; + feature?: KtxRuntimeFeature; +} + +export interface ManagedPythonSemanticLayerComputeOptions extends ManagedPythonCommandOptions { + createPythonCompute?: typeof createPythonSemanticLayerComputePort; +} + +export function managedRuntimeInstallCommand(feature: KtxRuntimeFeature): string { + return feature === 'local-embeddings' + ? 'ktx runtime install --feature local-embeddings --yes' + : 'ktx runtime install --yes'; +} + +function installPrompt(feature: KtxRuntimeFeature): string { + const label = feature === 'local-embeddings' ? 'local embeddings Python runtime' : 'core Python runtime'; + return `KTX needs to install the ${label}. This downloads Python dependencies with uv. Continue?`; +} + +function runtimeRequiredMessage(feature: KtxRuntimeFeature): string { + return `KTX Python runtime is required for this command. Run: ${managedRuntimeInstallCommand(feature)}`; +} + +function hasFeature(manifest: InstalledKtxRuntimeManifest, feature: KtxRuntimeFeature): boolean { + return manifest.features.includes(feature); +} + +async function defaultConfirmInstall(message: string): Promise { + if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) { + return false; + } + const response = await confirm({ message, initialValue: true }); + if (isCancel(response)) { + cancel('Runtime installation cancelled.'); + return false; + } + return response === true; +} + +export async function ensureManagedPythonCommandRuntime( + options: ManagedPythonCommandOptions, +): Promise { + const feature = options.feature ?? 'core'; + const readStatus = options.readStatus ?? readManagedPythonRuntimeStatus; + const installRuntime = options.installRuntime ?? installManagedPythonRuntime; + const status = await readStatus({ cliVersion: options.cliVersion }); + + if (status.kind === 'ready' && status.manifest && hasFeature(status.manifest, feature)) { + return { layout: status.layout, manifest: status.manifest }; + } + + if (options.installPolicy === 'never') { + throw new Error(runtimeRequiredMessage(feature)); + } + + if (options.installPolicy === 'prompt') { + const confirmInstall = options.confirmInstall ?? defaultConfirmInstall; + const confirmed = await confirmInstall(installPrompt(feature)); + if (!confirmed) { + throw new Error(`KTX Python runtime installation was cancelled. Run: ${managedRuntimeInstallCommand(feature)}`); + } + } + + options.io.stderr.write(`Installing KTX Python runtime (${feature}) with uv...\n`); + const installed = await installRuntime({ + cliVersion: options.cliVersion, + features: [feature], + force: false, + }); + options.io.stderr.write(`KTX Python runtime ready: ${installed.layout.versionDir}\n`); + return { layout: installed.layout, manifest: installed.manifest }; +} + +export async function createManagedPythonSemanticLayerComputePort( + options: ManagedPythonSemanticLayerComputeOptions, +): Promise { + const runtime = await ensureManagedPythonCommandRuntime({ + cliVersion: options.cliVersion, + installPolicy: options.installPolicy, + io: options.io, + feature: 'core', + ...(options.readStatus ? { readStatus: options.readStatus } : {}), + ...(options.installRuntime ? { installRuntime: options.installRuntime } : {}), + ...(options.confirmInstall ? { confirmInstall: options.confirmInstall } : {}), + }); + const createPythonCompute = options.createPythonCompute ?? createPythonSemanticLayerComputePort; + return createPythonCompute({ + command: runtime.manifest.python.daemonExecutable, + args: [], + }); +} +``` + +- [ ] **Step 2: Run the helper test** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/managed-python-command.test.ts +``` + +Expected: PASS. + +- [ ] **Step 3: Commit** + +Run: + +```bash +git add packages/cli/src/managed-python-command.ts packages/cli/src/managed-python-command.test.ts +git commit -m "feat: add managed python command helper" +``` + +Expected: commit succeeds. + +### Task 3: Add failing `runKtxSl` managed runtime tests + +**Files:** + +- Modify: `packages/cli/src/sl.test.ts` +- Test: `packages/cli/src/sl.test.ts` + +- [ ] **Step 1: Add runtime fields to existing `query` test args** + +In each existing `runKtxSl` call whose argument object has +`command: 'query'`, add these properties: + +```typescript +cliVersion: '0.2.0', +runtimeInstallPolicy: 'auto', +``` + +For example, the first `query` argument object becomes: + +```typescript +{ + command: 'query', + projectDir: '/tmp/project', + connectionId: 'warehouse', + query: { measures: ['orders.order_count'], dimensions: [] }, + format: 'sql', + execute: false, + cliVersion: '0.2.0', + runtimeInstallPolicy: 'auto', +} +``` + +- [ ] **Step 2: Add the managed helper delegation test** + +In `packages/cli/src/sl.test.ts`, add this test inside +`describe('runKtxSl', () => { ... })` after the existing +`runs sl query and prints SQL output` test: + +```typescript + it('creates default sl query compute through the managed runtime helper', async () => { + const projectDir = join(tempDir, 'project'); + const project = await initKtxProject({ projectDir, projectName: 'warehouse' }); + project.config.connections.warehouse = { driver: 'postgres', readonly: true }; + await project.fileStore.writeFile( + 'semantic-layer/warehouse/orders.yaml', + `name: orders +table: public.orders +grain: [id] +columns: + - name: id + type: number +measures: + - name: order_count + expr: count(*) +joins: [] +`, + 'ktx', + 'ktx@example.com', + 'Add orders source', + ); + + const stdout = { write: vi.fn() }; + const stderr = { write: vi.fn() }; + const compute = { + query: vi.fn(async () => ({ + sql: 'select count(*) as order_count from public.orders', + dialect: 'postgres', + columns: [{ name: 'orders.order_count' }], + plan: {}, + })), + validateSources: vi.fn(), + generateSources: vi.fn(), + }; + const createManagedSemanticLayerCompute = vi.fn(async () => compute); + + await expect( + runKtxSl( + { + command: 'query', + projectDir, + connectionId: 'warehouse', + query: { measures: ['orders.order_count'], dimensions: [] }, + format: 'sql', + execute: false, + cliVersion: '0.2.0', + runtimeInstallPolicy: 'auto', + }, + { stdout, stderr }, + { createManagedSemanticLayerCompute }, + ), + ).resolves.toBe(0); + + expect(createManagedSemanticLayerCompute).toHaveBeenCalledWith({ + cliVersion: '0.2.0', + installPolicy: 'auto', + io: { stdout, stderr }, + }); + expect(stdout.write).toHaveBeenCalledWith('select count(*) as order_count from public.orders\n'); + }); +``` + +- [ ] **Step 3: Run the failing `sl` test** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/sl.test.ts +``` + +Expected: FAIL with a TypeScript/Vitest error because `runKtxSl` does not +accept `createManagedSemanticLayerCompute` yet. + +### Task 4: Wire `runKtxSl` to the managed helper + +**Files:** + +- Modify: `packages/cli/src/sl.ts` +- Test: `packages/cli/src/sl.test.ts` + +- [ ] **Step 1: Add the managed helper imports** + +In `packages/cli/src/sl.ts`, add this import after the existing imports: + +```typescript +import { + createManagedPythonSemanticLayerComputePort, + type KtxManagedPythonInstallPolicy, +} from './managed-python-command.js'; +``` + +- [ ] **Step 2: Extend the `query` args type** + +In the `KtxSlArgs` union, replace the current `query` object type with this +shape: + +```typescript + | { + command: 'query'; + projectDir: string; + connectionId?: string; + query: SemanticLayerQueryInput; + format: SlQueryFormat; + execute: boolean; + maxRows?: number; + cliVersion: string; + runtimeInstallPolicy: KtxManagedPythonInstallPolicy; + }; +``` + +- [ ] **Step 3: Extend `KtxSlDeps`** + +In `packages/cli/src/sl.ts`, replace `KtxSlDeps` with this interface: + +```typescript +interface KtxSlDeps { + loadProject?: typeof loadKtxProject; + createSemanticLayerCompute?: () => KtxSemanticLayerComputePort; + createManagedSemanticLayerCompute?: (options: { + cliVersion: string; + installPolicy: KtxManagedPythonInstallPolicy; + io: KtxSlIo; + }) => Promise; + createQueryExecutor?: () => KtxSqlQueryExecutorPort; +} +``` + +- [ ] **Step 4: Use the managed helper in the `query` branch** + +In the `args.command === 'query'` branch, replace: + +```typescript + const compute = (deps.createSemanticLayerCompute ?? createPythonSemanticLayerComputePort)(); +``` + +with: + +```typescript + const compute = deps.createSemanticLayerCompute + ? deps.createSemanticLayerCompute() + : await (deps.createManagedSemanticLayerCompute ?? createManagedPythonSemanticLayerComputePort)({ + cliVersion: args.cliVersion, + installPolicy: args.runtimeInstallPolicy, + io, + }); +``` + +- [ ] **Step 5: Run the `sl` test** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/sl.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +Run: + +```bash +git add packages/cli/src/sl.ts packages/cli/src/sl.test.ts +git commit -m "feat: use managed runtime for sl query compute" +``` + +Expected: commit succeeds. + +### Task 5: Add failing Commander routing tests for `sl query` + +**Files:** + +- Modify: `packages/cli/src/index.test.ts` +- Test: `packages/cli/src/index.test.ts` + +- [ ] **Step 1: Add routing tests** + +In `packages/cli/src/index.test.ts`, add this test near the other command +routing tests: + +```typescript + it('routes sl query managed runtime install policies', async () => { + const sl = vi.fn(async () => 0); + + const promptIo = makeIo(); + await expect( + runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count'], promptIo.io, { sl }), + ).resolves.toBe(0); + expect(sl).toHaveBeenLastCalledWith( + expect.objectContaining({ + command: 'query', + projectDir: tempDir, + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'prompt', + query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }), + }), + promptIo.io, + ); + + const autoIo = makeIo(); + await expect( + runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes'], autoIo.io, { + sl, + }), + ).resolves.toBe(0); + expect(sl).toHaveBeenLastCalledWith( + expect.objectContaining({ + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'auto', + }), + autoIo.io, + ); + + const noInputIo = makeIo(); + await expect( + runKtxCli( + ['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--no-input'], + noInputIo.io, + { sl }, + ), + ).resolves.toBe(0); + expect(sl).toHaveBeenLastCalledWith( + expect.objectContaining({ + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'never', + }), + noInputIo.io, + ); + }); + + it('rejects conflicting sl query runtime install flags', async () => { + const io = makeIo(); + const sl = vi.fn(async () => 0); + + await expect( + runKtxCli( + ['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes', '--no-input'], + io.io, + { sl }, + ), + ).resolves.toBe(1); + + expect(sl).not.toHaveBeenCalled(); + expect(io.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input'); + }); +``` + +- [ ] **Step 2: Run the failing routing tests** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/index.test.ts +``` + +Expected: FAIL because `sl query` does not accept `--yes` or `--no-input` +and does not pass runtime policy fields yet. + +### Task 6: Wire `sl query` flags and schema validation + +**Files:** + +- Modify: `packages/cli/src/commands/sl-commands.ts` +- Modify: `packages/cli/src/command-schemas.ts` +- Test: `packages/cli/src/index.test.ts` + +- [ ] **Step 1: Add the runtime policy type import** + +In `packages/cli/src/commands/sl-commands.ts`, add this import: + +```typescript +import type { KtxManagedPythonInstallPolicy } from '../managed-python-command.js'; +``` + +- [ ] **Step 2: Add the runtime policy parser** + +In `packages/cli/src/commands/sl-commands.ts`, add this function near the +other option parsers: + +```typescript +function runtimeInstallPolicy(options: { yes?: boolean; input?: boolean }): KtxManagedPythonInstallPolicy { + if (options.yes === true && options.input === false) { + throw new Error('Choose only one runtime install mode: --yes or --no-input'); + } + if (options.yes === true) { + return 'auto'; + } + return options.input === false ? 'never' : 'prompt'; +} +``` + +- [ ] **Step 3: Add the command options** + +In the `sl.command('query')` option chain, add these options after +`.option('--execute', 'Execute the compiled query', false)`: + +```typescript + .option('--yes', 'Install the managed Python runtime without prompting when required', false) + .option('--no-input', 'Disable interactive managed runtime installation') +``` + +- [ ] **Step 4: Pass runtime fields into `slQueryCommandSchema.parse`** + +In the `sl.command('query')` action, add these properties to the parsed object: + +```typescript + cliVersion: context.packageInfo.version, + runtimeInstallPolicy: runtimeInstallPolicy(options), +``` + +The parsed object must include these fields next to `execute` and `format`: + +```typescript + const args = slQueryCommandSchema.parse({ + command: 'query', + projectDir: resolveCommandProjectDir(command), + connectionId: options.connectionId, + query: { + measures: options.measure, + dimensions: options.dimension, + ...(options.filter.length > 0 ? { filters: options.filter } : {}), + ...(options.segment.length > 0 ? { segments: options.segment } : {}), + ...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}), + ...(options.limit !== undefined ? { limit: options.limit } : {}), + ...(options.includeEmpty === true ? { include_empty: true } : {}), + }, + format: options.format, + execute: options.execute === true, + cliVersion: context.packageInfo.version, + runtimeInstallPolicy: runtimeInstallPolicy(options), + ...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}), + }); +``` + +- [ ] **Step 5: Extend the command schema** + +In `packages/cli/src/command-schemas.ts`, add these fields to +`slQueryCommandSchema` after `execute: z.boolean()`: + +```typescript + cliVersion: z.string().min(1), + runtimeInstallPolicy: z.enum(['prompt', 'auto', 'never']), +``` + +- [ ] **Step 6: Run the routing tests** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/index.test.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit** + +Run: + +```bash +git add packages/cli/src/commands/sl-commands.ts packages/cli/src/command-schemas.ts packages/cli/src/index.test.ts +git commit -m "feat: route sl query managed runtime policy" +``` + +Expected: commit succeeds. + +### Task 7: Verify the full changed surface + +**Files:** + +- Verify: `packages/cli/src/managed-python-command.test.ts` +- Verify: `packages/cli/src/sl.test.ts` +- Verify: `packages/cli/src/index.test.ts` +- Verify: `packages/cli/src/managed-python-command.ts` +- Verify: `packages/cli/src/sl.ts` +- Verify: `packages/cli/src/commands/sl-commands.ts` +- Verify: `packages/cli/src/command-schemas.ts` + +- [ ] **Step 1: Run focused CLI tests** + +Run: + +```bash +pnpm --filter @ktx/cli run test -- src/managed-python-command.test.ts src/sl.test.ts src/index.test.ts +``` + +Expected: PASS. + +- [ ] **Step 2: Run CLI type checking** + +Run: + +```bash +pnpm --filter @ktx/cli run type-check +``` + +Expected: PASS. + +- [ ] **Step 3: Run pre-commit for changed TypeScript files** + +Run: + +```bash +uv run pre-commit run --files packages/cli/src/managed-python-command.ts packages/cli/src/managed-python-command.test.ts packages/cli/src/sl.ts packages/cli/src/sl.test.ts packages/cli/src/commands/sl-commands.ts packages/cli/src/command-schemas.ts packages/cli/src/index.test.ts +``` + +Expected: PASS. If pre-commit is unavailable because the local `uv` version +does not satisfy `pyproject.toml`, record the version mismatch and run the +focused CLI tests plus type checking from Steps 1 and 2. + +- [ ] **Step 4: Commit verification fixes when needed** + +If Step 1, Step 2, or Step 3 changes files through formatting hooks, run: + +```bash +git add packages/cli/src/managed-python-command.ts packages/cli/src/managed-python-command.test.ts packages/cli/src/sl.ts packages/cli/src/sl.test.ts packages/cli/src/commands/sl-commands.ts packages/cli/src/command-schemas.ts packages/cli/src/index.test.ts +git commit -m "test: verify managed runtime sl query integration" +``` + +Expected: commit succeeds only when verification changed files. If no files +changed, leave the branch with the commits from Tasks 2, 4, and 6. + +## Acceptance criteria + +When this plan is complete: + +- `ktx sl query` uses the managed runtime's installed `ktx-daemon` executable + for semantic-layer compilation when no test compute dependency is injected. +- `ktx sl query --yes` installs the `core` runtime feature without prompting + when the managed runtime is missing. +- `ktx sl query --no-input` fails with + `KTX Python runtime is required for this command. Run: ktx runtime install --yes` + when the managed runtime is missing. +- `ktx sl query` prompts before first managed runtime installation in an + interactive terminal. +- Existing injected-compute tests still bypass runtime installation.