ktx/docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md
Andrey Avtomonov 9dad936ac7
feat: npm-managed Python runtime for @kaelio/ktx (#7)
* docs: add npm managed python runtime design

* build: add bundled python runtime wheel builder

* build: make local embedding dependencies optional

* build: bundle python runtime wheel in cli artifacts

* build: track bundled python runtime release artifact

* test: verify bundled python runtime wheel

* docs: add plan for bundled python runtime wheel

* test: cover managed python runtime lifecycle

* feat: add managed python runtime installer

* feat: add runtime command runner

* feat: expose runtime management commands

* test: verify managed python runtime commands

* docs: add plan for managed python runtime installer

* feat: add managed python command helper

* feat: use managed runtime for sl query compute

* feat: route sl query managed runtime policy

* docs: add plan for managed runtime sl query integration

* feat: add managed runtime daemon metadata

* feat: manage python daemon lifecycle

* feat: add runtime daemon start stop commands

* fix: verify managed runtime daemon lifecycle

* docs: add plan for managed runtime daemon lifecycle

* feat: add managed local embeddings config marker

* feat: add managed local embeddings daemon helper

* feat: use managed runtime for local embedding setup

* feat: pass managed runtime policy through setup

* docs: add plan for managed local embeddings runtime

* feat: read CLI package metadata dynamically

* feat: assemble public kaelio ktx npm package

* feat: release one public kaelio ktx npm artifact

* test: cover public kaelio ktx package invocations

* chore: verify public kaelio ktx package artifacts

* docs: add plan for public kaelio ktx npm package

* test: verify managed runtime in public package smoke

* test: finalize managed runtime release smoke

* docs: add plan for managed runtime release smoke

* test: specify local embeddings release smoke

* feat: add local embeddings runtime smoke

* chore: register local embeddings smoke

* fix: verify local embeddings smoke

* fix: restore artifact smoke python env helper

* docs: add plan for managed local embeddings release smoke

* refactor: share managed runtime install policy parsing

* feat: use managed runtime for agent semantic queries

* feat: use managed runtime for MCP semantic compute

* docs: add plan for managed agent and MCP semantic runtime

* feat(cli): add managed daemon HTTP helpers

* feat(cli): route local adapters through managed daemon

* feat(cli): use managed daemon for ingest helpers

* feat(cli): pass managed daemon options to scan

* feat(context): pass MCP ingest pull config options

* feat(cli): pass managed daemon options to serve ingest

* test: verify managed local ingest daemon runtime

* docs: add plan for managed local ingest daemon runtime

* docs: align managed runtime examples

* docs: add plan for managed runtime docs cleanup

* test: cover published package runtime smoke commands

* test: validate published package smoke outputs

* docs: add plan for published package runtime smoke

* build: stamp public npm package version

* release: add npm public release policy

* release: add guarded npm publish script

* release: document public npm release handoff

* docs: add plan for public npm release handoff

* test: cover managed runtime prune in package smoke

* docs: document managed runtime prune

* docs: add plan for managed runtime prune smoke and docs

* chore: encode uv runtime prerequisite policy

* fix: clarify missing uv runtime error

* docs: document uv runtime prerequisite

* docs: add plan for uv runtime prerequisite contract

* refactor: limit release artifacts to public package runtime

* chore: align release policy with bundled runtime wheel

* docs: describe single public runtime artifact surface

* test: verify single public runtime artifact contract

* docs: add plan for single public runtime artifact cleanup

* fix: align local embeddings smoke with public version

* docs: add plan for local embeddings smoke public version

* release: soft-launch as @kaelio/ktx@0.1.0-rc.0 on next tag

Publish target moves to the pre-release version 0.1.0-rc.0 under the next
dist-tag so npm install @kaelio/ktx (which resolves to latest) does not
pick up the soft-launch build. Users opt in via @kaelio/ktx@next.

* Fix release script boundary checks

* Remove PostHog from public package bundle
2026-05-11 15:50:34 +02:00

50 KiB

Managed Local Ingest Daemon Runtime 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 local ingest, scan, and MCP daemon-backed helper paths use the KTX-managed core Python daemon instead of requiring KTX_DAEMON_URL or a manually started daemon on 127.0.0.1:8765.

Architecture: Add lazy managed-daemon HTTP ports in the CLI package. Thread those ports through CLI local ingest adapter creation and pull-config options so Looker table identifier parsing, historic SQL analysis, and live-database daemon fallbacks resolve the managed core daemon only when a request is made.

Tech Stack: TypeScript, Vitest, Commander, KTX CLI managed Python runtime, KTX context local ingest adapters, MCP local project ports.


Existing status

This plan is based on docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md.

The following plans are based on that spec and are already implemented in this worktree:

  • docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md
  • docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md
  • docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md
  • docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md
  • docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md

Implementation evidence found before writing this plan includes:

  • scripts/build-python-runtime-wheel.mjs and packages/cli/assets/python/manifest.json.
  • packages/cli/src/managed-python-runtime.ts, packages/cli/src/runtime.ts, and packages/cli/src/commands/runtime-commands.ts.
  • packages/cli/src/managed-python-command.ts and managed ktx sl query, hidden agent SL query, and MCP semantic compute paths.
  • packages/cli/src/managed-python-daemon.ts and ktx runtime start / ktx runtime stop.
  • packages/cli/src/managed-local-embeddings.ts and local embeddings setup wiring.
  • scripts/build-public-npm-package.mjs, release policy updates, release smoke coverage, and opt-in local embeddings smoke coverage.
  • packages/cli/src/agent-runtime.ts and packages/cli/src/serve.ts now create managed semantic-layer compute when no explicit semantic HTTP URL is provided.

The remaining spec gap is local ingest daemon-backed helper behavior:

  • packages/context/src/ingest/local-adapters.ts still creates the Looker table identifier parser from options.looker.daemonBaseUrl, KTX_DAEMON_URL, or http://127.0.0.1:8765.
  • packages/cli/src/local-adapters.ts still creates historic SQL analysis from options.sqlAnalysisUrl, KTX_SQL_ANALYSIS_URL, KTX_DAEMON_URL, or http://127.0.0.1:8765.
  • packages/cli/src/serve.ts passes adapters to MCP local ingest, but LocalIngestMcpOptions has no pullConfigOptions, so Looker pull-config generation cannot receive CLI-managed daemon options.

This plan closes that gap without changing explicit daemon URL behavior. Explicit --database-introspection-url, explicit test dependency injection, KTX_SQL_ANALYSIS_URL, and KTX_DAEMON_URL continue to win over the managed daemon.

File structure

  • Create packages/cli/src/managed-python-http.ts: lazy managed core daemon resolver, generic HTTP JSON runner, managed Looker table identifier parser, managed SQL analysis port, and managed live-database daemon request options.
  • Create packages/cli/src/managed-python-http.test.ts: verifies lazy daemon resolution, install policy propagation, daemon reuse caching, and HTTP runner delegation.
  • Modify packages/cli/src/local-adapters.ts: accepts managed daemon options and wires them into daemon-backed local ingest helpers only when no explicit daemon URL is configured.
  • Modify packages/cli/src/ingest.ts: adds runtime install policy fields to run args and passes managed daemon options to both adapter creation and local pull-config resolution.
  • Modify packages/cli/src/ingest.test.ts: covers managed daemon option threading and preserves explicit daemon URL behavior.
  • Modify packages/cli/src/commands/ingest-commands.ts: adds --yes to ktx ingest run and uses existing --no-input as the runtime noninteractive mode.
  • Modify packages/cli/src/scan.ts: adds runtime install policy fields and passes managed daemon options to local ingest adapters used during scan.
  • Modify packages/cli/src/scan.test.ts: covers managed daemon option threading and explicit daemon URL behavior.
  • Modify packages/cli/src/commands/scan-commands.ts: adds --yes and --no-input to ktx scan.
  • Modify packages/context/src/ingest/local-ingest.ts: adds pullConfigOptions to LocalIngestMcpOptions.
  • Modify packages/context/src/mcp/local-project-ports.ts: passes MCP local ingest pull-config options into runLocalIngest().
  • Modify packages/context/src/mcp/local-project-ports.test.ts: covers MCP pull-config option forwarding.
  • Modify packages/cli/src/serve.ts: passes managed daemon options and pull-config options to MCP local ingest.
  • Modify packages/cli/src/serve.test.ts: covers MCP local ingest managed daemon option wiring.
  • Modify packages/cli/src/index.test.ts: updates Commander routing expectations for ingest and scan runtime install policy flags.

Task 1: Add managed daemon HTTP helpers

Files:

  • Create: packages/cli/src/managed-python-http.test.ts

  • Create: packages/cli/src/managed-python-http.ts

  • Test: packages/cli/src/managed-python-http.test.ts

  • Step 1: Write failing tests for lazy daemon HTTP helpers

Create packages/cli/src/managed-python-http.test.ts with this content:

import { describe, expect, it, vi } from 'vitest';
import {
  createManagedDaemonHttpJsonRunner,
  createManagedDaemonLookerTableIdentifierParser,
  createManagedDaemonSqlAnalysisPort,
  createManagedPythonDaemonBaseUrlResolver,
  managedDaemonDatabaseIntrospectionOptions,
} from './managed-python-http.js';

function io() {
  let stderr = '';
  return {
    io: {
      stdout: { write: vi.fn() },
      stderr: { write: (chunk: string) => (stderr += chunk) },
    },
    stderr: () => stderr,
  };
}

describe('createManagedPythonDaemonBaseUrlResolver', () => {
  it('ensures the core runtime, starts the daemon, reports the URL, and caches the result', async () => {
    const testIo = io();
    const ensureRuntime = vi.fn(async () => ({
      layout: {} as never,
      manifest: {} as never,
    }));
    const startDaemon = vi.fn(async () => ({
      status: 'started' as const,
      layout: {} as never,
      state: { pid: 1234 } as never,
      baseUrl: 'http://127.0.0.1:61234',
    }));
    const resolveBaseUrl = createManagedPythonDaemonBaseUrlResolver({
      cliVersion: '0.2.0',
      installPolicy: 'auto',
      io: testIo.io,
      ensureRuntime,
      startDaemon,
    });

    await expect(resolveBaseUrl()).resolves.toBe('http://127.0.0.1:61234');
    await expect(resolveBaseUrl()).resolves.toBe('http://127.0.0.1:61234');

    expect(ensureRuntime).toHaveBeenCalledTimes(1);
    expect(ensureRuntime).toHaveBeenCalledWith({
      cliVersion: '0.2.0',
      installPolicy: 'auto',
      io: testIo.io,
      feature: 'core',
    });
    expect(startDaemon).toHaveBeenCalledTimes(1);
    expect(startDaemon).toHaveBeenCalledWith({
      cliVersion: '0.2.0',
      features: ['core'],
      force: false,
    });
    expect(testIo.stderr()).toContain('Started KTX Python daemon: http://127.0.0.1:61234');
  });

  it('reports daemon reuse without reinstalling after the first resolved URL', async () => {
    const testIo = io();
    const ensureRuntime = vi.fn(async () => ({
      layout: {} as never,
      manifest: {} as never,
    }));
    const startDaemon = vi.fn(async () => ({
      status: 'reused' as const,
      layout: {} as never,
      state: { pid: 1234 } as never,
      baseUrl: 'http://127.0.0.1:61234',
    }));
    const resolveBaseUrl = createManagedPythonDaemonBaseUrlResolver({
      cliVersion: '0.2.0',
      installPolicy: 'never',
      io: testIo.io,
      ensureRuntime,
      startDaemon,
    });

    await expect(resolveBaseUrl()).resolves.toBe('http://127.0.0.1:61234');
    await expect(resolveBaseUrl()).resolves.toBe('http://127.0.0.1:61234');

    expect(ensureRuntime).toHaveBeenCalledTimes(1);
    expect(startDaemon).toHaveBeenCalledTimes(1);
    expect(testIo.stderr()).toContain('Using existing KTX Python daemon: http://127.0.0.1:61234');
  });
});

describe('createManagedDaemonHttpJsonRunner', () => {
  it('resolves the managed base URL lazily for each HTTP JSON request', async () => {
    const postJson = vi.fn(async () => ({ ok: true }));
    const runner = createManagedDaemonHttpJsonRunner({
      resolveBaseUrl: async () => 'http://127.0.0.1:61234',
      postJson,
    });

    await expect(runner('/sql/parse-table-identifier', { items: [] })).resolves.toEqual({ ok: true });

    expect(postJson).toHaveBeenCalledWith('http://127.0.0.1:61234', '/sql/parse-table-identifier', { items: [] });
  });
});

describe('managed daemon ingest ports', () => {
  it('creates a Looker table parser backed by the managed daemon runner', async () => {
    const requestJson = vi.fn(async () => ({
      results: {
        'model.explore': {
          ok: true,
          catalog: 'warehouse',
          schema: 'public',
          name: 'orders',
          canonical_table: 'public.orders',
        },
      },
    }));
    const parser = createManagedDaemonLookerTableIdentifierParser({ requestJson });

    await expect(
      parser.parse([{ key: 'model.explore', sql_table_name: 'public.orders', dialect: 'postgres' }]),
    ).resolves.toEqual({
      'model.explore': {
        ok: true,
        catalog: 'warehouse',
        schema: 'public',
        name: 'orders',
        canonical_table: 'public.orders',
      },
    });
    expect(requestJson).toHaveBeenCalledWith('/sql/parse-table-identifier', {
      items: [{ key: 'model.explore', sql_table_name: 'public.orders', dialect: 'postgres' }],
    });
  });

  it('creates a SQL analysis port backed by the managed daemon runner', async () => {
    const requestJson = vi.fn(async () => ({
      fingerprint: 'select-orders',
      normalized_sql: 'SELECT * FROM public.orders WHERE id = ?',
      tables_touched: ['public.orders'],
      literal_slots: [{ position: 1, type: 'number', example_value: '42' }],
    }));
    const sqlAnalysis = createManagedDaemonSqlAnalysisPort({ requestJson });

    await expect(sqlAnalysis.analyzeForFingerprint('SELECT * FROM public.orders WHERE id = 42', 'postgres')).resolves
      .toEqual({
        fingerprint: 'select-orders',
        normalizedSql: 'SELECT * FROM public.orders WHERE id = ?',
        tablesTouched: ['public.orders'],
        literalSlots: [{ position: 1, type: 'number', exampleValue: '42' }],
      });
    expect(requestJson).toHaveBeenCalledWith('/api/sql/analyze-for-fingerprint', {
      sql: 'SELECT * FROM public.orders WHERE id = 42',
      dialect: 'postgres',
    });
  });

  it('returns live-database daemon request options backed by the managed runner', async () => {
    const requestJson = vi.fn(async () => ({
      connection_id: 'warehouse',
      tables: [],
    }));
    const options = managedDaemonDatabaseIntrospectionOptions({ requestJson });

    await expect(options.requestJson('/database/introspect', { connection_id: 'warehouse' })).resolves.toEqual({
      connection_id: 'warehouse',
      tables: [],
    });
    expect(requestJson).toHaveBeenCalledWith('/database/introspect', { connection_id: 'warehouse' });
  });
});
  • Step 2: Run the failing helper tests

Run:

pnpm --filter @ktx/cli run test -- src/managed-python-http.test.ts

Expected: FAIL with an import error for ./managed-python-http.js.

  • Step 3: Implement managed daemon HTTP helpers

Create packages/cli/src/managed-python-http.ts with this content:

import { request as httpRequest } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { URL } from 'node:url';
import {
  createDaemonLookerTableIdentifierParser,
  type DaemonLiveDatabaseIntrospectionOptions,
  type KtxDaemonDatabaseHttpJsonRunner,
  type KtxDaemonTableIdentifierHttpJsonRunner,
  type LookerTableIdentifierParser,
} from '@ktx/context/ingest';
import {
  createHttpSqlAnalysisPort,
  type KtxSqlAnalysisHttpJsonRunner,
  type SqlAnalysisPort,
} from '@ktx/context/sql-analysis';
import type { KtxCliIo } from './cli-runtime.js';
import {
  ensureManagedPythonCommandRuntime,
  type KtxManagedPythonInstallPolicy,
  type ManagedPythonCommandRuntime,
} from './managed-python-command.js';
import { startManagedPythonDaemon, type ManagedPythonDaemonStartResult } from './managed-python-daemon.js';

export type ManagedPythonHttpJsonRunner = (
  path: string,
  payload: Record<string, unknown>,
) => Promise<Record<string, unknown>>;

export type ManagedPythonHttpPostJson = (
  baseUrl: string,
  path: string,
  payload: Record<string, unknown>,
) => Promise<Record<string, unknown>>;

export interface ManagedPythonCoreDaemonOptions {
  cliVersion: string;
  installPolicy: KtxManagedPythonInstallPolicy;
  io: KtxCliIo;
  ensureRuntime?: (options: {
    cliVersion: string;
    installPolicy: KtxManagedPythonInstallPolicy;
    io: KtxCliIo;
    feature: 'core';
  }) => Promise<ManagedPythonCommandRuntime>;
  startDaemon?: (options: {
    cliVersion: string;
    features: ['core'];
    force: false;
  }) => Promise<ManagedPythonDaemonStartResult>;
}

export type ManagedPythonDaemonHttpOptions =
  | {
      requestJson: ManagedPythonHttpJsonRunner;
    }
  | {
      resolveBaseUrl: () => Promise<string>;
      postJson?: ManagedPythonHttpPostJson;
    }
  | (ManagedPythonCoreDaemonOptions & {
      postJson?: ManagedPythonHttpPostJson;
    });

function normalizedBaseUrl(baseUrl: string): string {
  return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
}

function parseJsonObject(raw: string, path: string): Record<string, unknown> {
  const parsed = JSON.parse(raw) as unknown;
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`KTX managed daemon HTTP ${path} returned non-object JSON`);
  }
  return parsed as Record<string, unknown>;
}

export async function postManagedDaemonJson(
  baseUrl: string,
  path: string,
  payload: Record<string, unknown>,
): Promise<Record<string, unknown>> {
  return await new Promise((resolve, reject) => {
    const target = new URL(path.replace(/^\//, ''), normalizedBaseUrl(baseUrl));
    const body = JSON.stringify(payload);
    const client = target.protocol === 'https:' ? httpsRequest : httpRequest;
    const request = client(
      target,
      {
        method: 'POST',
        headers: {
          accept: 'application/json',
          'content-type': 'application/json',
          'content-length': Buffer.byteLength(body),
        },
      },
      (response) => {
        const chunks: Buffer[] = [];
        response.on('data', (chunk: Buffer) => chunks.push(chunk));
        response.on('end', () => {
          const text = Buffer.concat(chunks).toString('utf8');
          const statusCode = response.statusCode ?? 0;
          if (statusCode < 200 || statusCode >= 300) {
            reject(new Error(`KTX managed daemon HTTP ${path} failed with ${statusCode}: ${text}`));
            return;
          }
          try {
            resolve(parseJsonObject(text, path));
          } catch (error) {
            reject(error);
          }
        });
      },
    );
    request.on('error', reject);
    request.end(body);
  });
}

export function createManagedPythonDaemonBaseUrlResolver(
  options: ManagedPythonCoreDaemonOptions,
): () => Promise<string> {
  let cachedBaseUrl: string | undefined;

  return async () => {
    if (cachedBaseUrl) {
      return cachedBaseUrl;
    }

    const ensureRuntime = options.ensureRuntime ?? ensureManagedPythonCommandRuntime;
    const startDaemon = options.startDaemon ?? startManagedPythonDaemon;
    await ensureRuntime({
      cliVersion: options.cliVersion,
      installPolicy: options.installPolicy,
      io: options.io,
      feature: 'core',
    });
    const daemon = await startDaemon({
      cliVersion: options.cliVersion,
      features: ['core'],
      force: false,
    });
    const verb = daemon.status === 'started' ? 'Started' : 'Using existing';
    options.io.stderr.write(`${verb} KTX Python daemon: ${daemon.baseUrl}\n`);
    cachedBaseUrl = daemon.baseUrl;
    return cachedBaseUrl;
  };
}

function isRequestJsonOnly(options: ManagedPythonDaemonHttpOptions): options is { requestJson: ManagedPythonHttpJsonRunner } {
  return 'requestJson' in options;
}

function isResolveBaseUrlOnly(
  options: ManagedPythonDaemonHttpOptions,
): options is { resolveBaseUrl: () => Promise<string>; postJson?: ManagedPythonHttpPostJson } {
  return 'resolveBaseUrl' in options;
}

export function createManagedDaemonHttpJsonRunner(
  options: ManagedPythonDaemonHttpOptions,
): ManagedPythonHttpJsonRunner {
  if (isRequestJsonOnly(options)) {
    return options.requestJson;
  }
  const resolveBaseUrl = isResolveBaseUrlOnly(options)
    ? options.resolveBaseUrl
    : createManagedPythonDaemonBaseUrlResolver(options);
  const postJson = options.postJson ?? postManagedDaemonJson;

  return async (path, payload) => postJson(await resolveBaseUrl(), path, payload);
}

export function createManagedDaemonLookerTableIdentifierParser(
  options: ManagedPythonDaemonHttpOptions,
): LookerTableIdentifierParser {
  return createDaemonLookerTableIdentifierParser({
    baseUrl: 'http://127.0.0.1:0',
    requestJson: createManagedDaemonHttpJsonRunner(options) as KtxDaemonTableIdentifierHttpJsonRunner,
  });
}

export function createManagedDaemonSqlAnalysisPort(options: ManagedPythonDaemonHttpOptions): SqlAnalysisPort {
  return createHttpSqlAnalysisPort({
    baseUrl: 'http://127.0.0.1:0',
    requestJson: createManagedDaemonHttpJsonRunner(options) as KtxSqlAnalysisHttpJsonRunner,
  });
}

export function managedDaemonDatabaseIntrospectionOptions(
  options: ManagedPythonDaemonHttpOptions,
): Pick<DaemonLiveDatabaseIntrospectionOptions, 'requestJson'> {
  return {
    requestJson: createManagedDaemonHttpJsonRunner(options) as KtxDaemonDatabaseHttpJsonRunner,
  };
}
  • Step 4: Verify the helper tests pass

Run:

pnpm --filter @ktx/cli run test -- src/managed-python-http.test.ts

Expected: PASS.

  • Step 5: Commit the helper

Run:

git add packages/cli/src/managed-python-http.ts packages/cli/src/managed-python-http.test.ts
git commit -m "feat(cli): add managed daemon HTTP helpers"

Expected: commit succeeds.

Task 2: Wire managed daemon options into CLI local adapters

Files:

  • Modify: packages/cli/src/local-adapters.ts

  • Test: packages/cli/src/managed-python-http.test.ts

  • Step 1: Update local adapter imports

In packages/cli/src/local-adapters.ts, add this import after the createHttpSqlAnalysisPort import:

import {
  createManagedDaemonLookerTableIdentifierParser,
  createManagedDaemonSqlAnalysisPort,
  managedDaemonDatabaseIntrospectionOptions,
  type ManagedPythonCoreDaemonOptions,
} from './managed-python-http.js';
  • Step 2: Add managed daemon options to the local adapter option type

Replace this interface:

interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
  historicSqlConnectionId?: string;
  sqlAnalysisUrl?: string;
}

with this interface:

export interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
  historicSqlConnectionId?: string;
  sqlAnalysisUrl?: string;
  managedDaemon?: ManagedPythonCoreDaemonOptions;
}
  • Step 3: Add helper functions for managed daemon adapter options

Add these helpers immediately after hasSnowflakeDriver():

function ktxCliDaemonDatabaseIntrospectionOptions(
  options: KtxCliLocalIngestAdaptersOptions,
): DefaultLocalIngestAdaptersOptions['databaseIntrospection'] {
  if (options.databaseIntrospectionUrl || options.databaseIntrospection?.requestJson || !options.managedDaemon) {
    return options.databaseIntrospection;
  }
  return {
    ...(options.databaseIntrospection ?? {}),
    ...managedDaemonDatabaseIntrospectionOptions(options.managedDaemon),
  };
}

function ktxCliLookerOptions(
  options: KtxCliLocalIngestAdaptersOptions,
): DefaultLocalIngestAdaptersOptions['looker'] {
  const looker = options.looker;
  if (looker?.parser || looker?.daemonBaseUrl || process.env.KTX_DAEMON_URL || !options.managedDaemon) {
    return looker;
  }
  return {
    ...(looker ?? {}),
    parser: createManagedDaemonLookerTableIdentifierParser(options.managedDaemon),
  };
}

function ktxCliHistoricSqlAnalysis(options: KtxCliLocalIngestAdaptersOptions) {
  if (options.sqlAnalysisUrl) {
    return createHttpSqlAnalysisPort({ baseUrl: options.sqlAnalysisUrl });
  }
  if (process.env.KTX_SQL_ANALYSIS_URL) {
    return createHttpSqlAnalysisPort({ baseUrl: process.env.KTX_SQL_ANALYSIS_URL });
  }
  if (process.env.KTX_DAEMON_URL) {
    return createHttpSqlAnalysisPort({ baseUrl: process.env.KTX_DAEMON_URL });
  }
  if (options.managedDaemon) {
    return createManagedDaemonSqlAnalysisPort(options.managedDaemon);
  }
  return createHttpSqlAnalysisPort({ baseUrl: 'http://127.0.0.1:8765' });
}
  • Step 4: Use managed daemon request options for daemon live-database fallback

In createKtxCliLiveDatabaseIntrospection(), insert this line before the const daemon = createDaemonLiveDatabaseIntrospection({ statement:

  const databaseIntrospection = ktxCliDaemonDatabaseIntrospectionOptions(options);

Then replace the daemon creation block:

  const daemon = createDaemonLiveDatabaseIntrospection({
    connections: project.config.connections,
    ...options.databaseIntrospection,
    ...(options.databaseIntrospectionUrl ? { baseUrl: options.databaseIntrospectionUrl } : {}),
  });

with this block:

  const daemon = createDaemonLiveDatabaseIntrospection({
    connections: project.config.connections,
    ...databaseIntrospection,
    ...(options.databaseIntrospectionUrl ? { baseUrl: options.databaseIntrospectionUrl } : {}),
  });
  • Step 5: Use managed daemon SQL analysis for historic SQL

In historicSqlOptionsForLocalRun(), replace this block:

  return {
    sqlAnalysis: createHttpSqlAnalysisPort({
      baseUrl:
        options.sqlAnalysisUrl ??
        process.env.KTX_SQL_ANALYSIS_URL ??
        process.env.KTX_DAEMON_URL ??
        'http://127.0.0.1:8765',
    }),
    postgresQueryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
    postgresBaselineRootDir: join(project.projectDir, '.ktx/cache/historic-sql'),
  };

with this block:

  return {
    sqlAnalysis: ktxCliHistoricSqlAnalysis(options),
    postgresQueryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
    postgresBaselineRootDir: join(project.projectDir, '.ktx/cache/historic-sql'),
  };
  • Step 6: Pass managed Looker options into default local adapters

In createKtxCliLocalIngestAdapters(), replace:

  const base = createDefaultLocalIngestAdapters(project, {
    ...options,
    ...(historicSql ? { historicSql } : {}),
  });

with:

  const base = createDefaultLocalIngestAdapters(project, {
    ...options,
    databaseIntrospection: ktxCliDaemonDatabaseIntrospectionOptions(options),
    looker: ktxCliLookerOptions(options),
    ...(historicSql ? { historicSql } : {}),
  });
  • Step 7: Run the CLI type check for local adapter changes

Run:

pnpm --filter @ktx/cli run type-check

Expected: PASS.

  • Step 8: Commit local adapter wiring

Run:

git add packages/cli/src/local-adapters.ts
git commit -m "feat(cli): route local adapters through managed daemon"

Expected: commit succeeds.

Task 3: Thread managed daemon options through ingest commands

Files:

  • Modify: packages/cli/src/ingest.ts

  • Modify: packages/cli/src/ingest.test.ts

  • Modify: packages/cli/src/commands/ingest-commands.ts

  • Test: packages/cli/src/ingest.test.ts

  • Test: packages/cli/src/index.test.ts

  • Step 1: Write failing ingest option-threading tests

In packages/cli/src/ingest.test.ts, add this test after passes daemon database introspection URL to default local ingest adapters:

  it('passes managed daemon options to adapters and pull-config options when no explicit daemon URL is set', async () => {
    const projectDir = join(tempDir, 'managed-daemon-ingest-project');
    await initKtxProject({ projectDir, projectName: 'managed-daemon-ingest-project' });
    await writeWarehouseConfig(projectDir);
    const createdAdapters: SourceAdapter[] = [
      { source: 'fake', skillNames: [], detect: async () => true, chunk: async () => ({ workUnits: [] }) },
    ];
    const createAdapters = vi.fn(() => createdAdapters as never);
    const runLocal = vi.fn(async (input: RunLocalIngestOptions) =>
      completedLocalBundleRun(input, input.jobId ?? 'local-job-1'),
    );
    const io = makeIo();

    await expect(
      runKtxIngest(
        {
          command: 'run',
          projectDir,
          connectionId: 'warehouse',
          adapter: 'fake',
          cliVersion: '0.2.0',
          runtimeInstallPolicy: 'auto',
          outputMode: 'plain',
        } satisfies KtxIngestArgs,
        io.io,
        {
          createAdapters,
          runLocalIngest: runLocal,
          jobIdFactory: () => 'local-job-1',
        },
      ),
    ).resolves.toBe(0);

    const expectedManagedDaemon = {
      cliVersion: '0.2.0',
      installPolicy: 'auto',
      io: io.io,
    };
    expect(createAdapters).toHaveBeenCalledWith(expect.objectContaining({ projectDir }), {
      managedDaemon: expectedManagedDaemon,
    });
    expect(runLocal).toHaveBeenCalledWith(
      expect.objectContaining({
        pullConfigOptions: {
          managedDaemon: expectedManagedDaemon,
        },
      }),
    );
  });

In the existing passes daemon database introspection URL to default local ingest adapters test, add this assertion inside the existing expect(runLocal) block:

        pullConfigOptions: {
          databaseIntrospectionUrl: 'http://127.0.0.1:8765',
        },
  • Step 2: Run the failing ingest tests

Run:

pnpm --filter @ktx/cli run test -- src/ingest.test.ts

Expected: FAIL because KtxIngestArgs has no cliVersion or runtimeInstallPolicy, and runKtxIngest() does not pass managed daemon options into createAdapters() or pullConfigOptions.

  • Step 3: Add runtime install policy fields to ingest args

In packages/cli/src/ingest.ts, add this import after the local adapters import:

import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';

In the KtxIngestArgs command: 'run' branch, add these fields after databaseIntrospectionUrl?: string;:

      cliVersion?: string;
      runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
  • Step 4: Add a managed daemon option helper to ingest

In packages/cli/src/ingest.ts, add this helper after initialRunMemoryFlowInput():

function managedDaemonOptionsForIngestRun(
  args: Extract<KtxIngestArgs, { command: 'run' }>,
  io: KtxIngestIo,
) {
  if (args.databaseIntrospectionUrl || !args.cliVersion || !args.runtimeInstallPolicy) {
    return undefined;
  }
  return {
    cliVersion: args.cliVersion,
    installPolicy: args.runtimeInstallPolicy,
    io,
  };
}
  • Step 5: Pass managed daemon options to adapters and pull-config resolution

In the args.command === 'run' branch of runKtxIngest(), replace the adapterOptions block:

      const adapterOptions = {
        ...(localIngestOptions.pullConfigOptions ?? {}),
        ...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
        ...(args.adapter === 'historic-sql' ? { historicSqlConnectionId: args.connectionId } : {}),
      };

with:

      const managedDaemon = managedDaemonOptionsForIngestRun(args, io);
      const adapterOptions = {
        ...(localIngestOptions.pullConfigOptions ?? {}),
        ...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
        ...(managedDaemon ? { managedDaemon } : {}),
        ...(args.adapter === 'historic-sql' ? { historicSqlConnectionId: args.connectionId } : {}),
      };

In the non-Metabase executeLocalIngest() call, move ...localIngestOptions before pullConfigOptions and add pullConfigOptions: adapterOptions. The call must contain this sequence after the edit:

        const result = await executeLocalIngest({
          project,
          adapters: createAdapters(project, adapterOptions),
          adapter: args.adapter,
          connectionId: args.connectionId,
          sourceDir: args.sourceDir,
          trigger: 'manual_resync',
          jobId,
          ...localIngestOptions,
          pullConfigOptions: adapterOptions,
          ...(args.debugLlmRequestFile ? { llmDebugRequestFile: args.debugLlmRequestFile } : {}),
          ...(memoryFlow ? { memoryFlow } : {}),
        });
  • Step 6: Add runtime flags to ktx ingest run routing

In packages/cli/src/commands/ingest-commands.ts, add this import after the KtxCliDeps import:

import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';

In the ingest run command options, add this option immediately before .option('--no-input', ...):

    .option('--yes', 'Install the managed Python runtime without prompting when required', false)

In the KtxIngestArgs object built for ingest run, add these fields after databaseIntrospectionUrl: options.databaseIntrospectionUrl || undefined,:

          cliVersion: context.packageInfo.version,
          runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
  • Step 7: Update Commander ingest routing expectations

In packages/cli/src/index.test.ts, in the test that routes dev ingest run, add these expected fields after databaseIntrospectionUrl: undefined,:

        cliVersion: '0.0.0-private',
        runtimeInstallPolicy: 'never',

Add this test after that existing routing test:

  it('routes ingest managed runtime install policies', async () => {
    const autoIo = makeIo();
    const conflictIo = makeIo();
    const ingest = vi.fn(async () => 0);

    await expect(
      runKtxCli(
        [
          'dev',
          'ingest',
          'run',
          '--project-dir',
          tempDir,
          '--connection-id',
          'warehouse',
          '--adapter',
          'looker',
          '--yes',
        ],
        autoIo.io,
        { ingest },
      ),
    ).resolves.toBe(0);
    await expect(
      runKtxCli(
        [
          'dev',
          'ingest',
          'run',
          '--project-dir',
          tempDir,
          '--connection-id',
          'warehouse',
          '--adapter',
          'looker',
          '--yes',
          '--no-input',
        ],
        conflictIo.io,
        { ingest },
      ),
    ).resolves.toBe(1);

    expect(ingest).toHaveBeenCalledWith(
      expect.objectContaining({
        command: 'run',
        cliVersion: '0.0.0-private',
        runtimeInstallPolicy: 'auto',
      }),
      autoIo.io,
    );
    expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
  });
  • Step 8: Run focused ingest and routing tests

Run:

pnpm --filter @ktx/cli run test -- src/ingest.test.ts src/index.test.ts

Expected: PASS.

  • Step 9: Commit ingest runtime policy wiring

Run:

git add packages/cli/src/ingest.ts packages/cli/src/ingest.test.ts packages/cli/src/commands/ingest-commands.ts packages/cli/src/index.test.ts
git commit -m "feat(cli): use managed daemon for ingest helpers"

Expected: commit succeeds.

Task 4: Thread managed daemon options through scan commands

Files:

  • Modify: packages/cli/src/scan.ts

  • Modify: packages/cli/src/scan.test.ts

  • Modify: packages/cli/src/commands/scan-commands.ts

  • Modify: packages/cli/src/index.test.ts

  • Test: packages/cli/src/scan.test.ts

  • Test: packages/cli/src/index.test.ts

  • Step 1: Write failing scan option-threading test

In packages/cli/src/scan.test.ts, add this test after the test that passes databaseIntrospectionUrl:

  it('passes managed daemon options to local ingest adapters when no explicit daemon URL is set', async () => {
    const report = minimalScanReport();
    const createLocalIngestAdapters = vi.fn(() => []);
    const runLocalScan = vi.fn(
      async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
        runId: 'scan-run-1',
        status: 'done',
        done: true,
        connectionId: 'warehouse',
        mode: 'structural',
        dryRun: false,
        syncId: 'sync-1',
        report,
      }),
    );
    const io = makeIo();

    await expect(
      runKtxScan(
        {
          command: 'run',
          projectDir: tempDir,
          connectionId: 'warehouse',
          mode: 'structural',
          detectRelationships: false,
          dryRun: false,
          cliVersion: '0.2.0',
          runtimeInstallPolicy: 'auto',
        },
        io.io,
        { runLocalScan, createLocalIngestAdapters },
      ),
    ).resolves.toBe(0);

    expect(createLocalIngestAdapters).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir }), {
      managedDaemon: {
        cliVersion: '0.2.0',
        installPolicy: 'auto',
        io: io.io,
      },
    });
  });
  • Step 2: Run the failing scan tests

Run:

pnpm --filter @ktx/cli run test -- src/scan.test.ts

Expected: FAIL because KtxScanArgs has no cliVersion or runtimeInstallPolicy, and runKtxScan() does not pass managed daemon options to adapter creation.

  • Step 3: Add runtime install policy fields to scan args

In packages/cli/src/scan.ts, add this import after the local adapter import:

import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';

In the KtxScanArgs command: 'run' branch, add these fields after databaseIntrospectionUrl?: string;:

      cliVersion?: string;
      runtimeInstallPolicy?: KtxManagedPythonInstallPolicy;
  • Step 4: Add managed daemon option construction to scan

In packages/cli/src/scan.ts, add this helper after warningLine():

function managedDaemonOptionsForScanRun(args: Extract<KtxScanArgs, { command: 'run' }>, io: KtxCliIo) {
  if (args.databaseIntrospectionUrl || !args.cliVersion || !args.runtimeInstallPolicy) {
    return undefined;
  }
  return {
    cliVersion: args.cliVersion,
    installPolicy: args.runtimeInstallPolicy,
    io,
  };
}

In the runLocalScan() call, replace this adapter creation block:

        adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, {
          databaseIntrospectionUrl: args.databaseIntrospectionUrl,
        }),

with:

        adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, {
          ...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
          ...(managedDaemonOptionsForScanRun(args, io)
            ? { managedDaemon: managedDaemonOptionsForScanRun(args, io) }
            : {}),
        }),

Then replace the repeated helper call with a local constant to keep the code single-pass. The final block must be:

    const managedDaemon = managedDaemonOptionsForScanRun(args, io);
    const connector =
      args.mode !== 'structural' || args.detectRelationships
        ? await createKtxCliScanConnector(project, args.connectionId)
        : undefined;
    const progress = createCliScanProgress(io);
    try {
      const result = await (deps.runLocalScan ?? runLocalScan)({
        project,
        connectionId: args.connectionId,
        mode: args.mode,
        detectRelationships: args.detectRelationships,
        dryRun: args.dryRun,
        trigger: 'cli',
        databaseIntrospectionUrl: args.databaseIntrospectionUrl,
        connector,
        adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, {
          ...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
          ...(managedDaemon ? { managedDaemon } : {}),
        }),
        progress,
      });
  • Step 5: Add runtime flags to scan routing

In packages/cli/src/commands/scan-commands.ts, add this import after the cli-program.js import:

import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';

In the top-level scan command options, add these options after --database-introspection-url:

    .option('--yes', 'Install the managed Python runtime without prompting when required', false)
    .option('--no-input', 'Disable interactive managed runtime installation')

In the scan run action, add these fields after databaseIntrospectionUrl: options.databaseIntrospectionUrl,:

        cliVersion: context.packageInfo.version,
        runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
  • Step 6: Update Commander scan routing expectations

In packages/cli/src/index.test.ts, update the routes low-level scan through ktx dev with top-level project-dir expected args by adding:

        cliVersion: '0.0.0-private',
        runtimeInstallPolicy: 'prompt',

Add this test after that routing test:

  it('routes scan managed runtime install policies', async () => {
    const autoIo = makeIo();
    const neverIo = makeIo();
    const conflictIo = makeIo();
    const scan = vi.fn().mockResolvedValue(0);

    await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse', '--yes'], autoIo.io, { scan }))
      .resolves.toBe(0);
    await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse', '--no-input'], neverIo.io, { scan }))
      .resolves.toBe(0);
    await expect(
      runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse', '--yes', '--no-input'], conflictIo.io, {
        scan,
      }),
    ).resolves.toBe(1);

    expect(scan).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        command: 'run',
        runtimeInstallPolicy: 'auto',
      }),
      autoIo.io,
    );
    expect(scan).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        command: 'run',
        runtimeInstallPolicy: 'never',
      }),
      neverIo.io,
    );
    expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
  });
  • Step 7: Run focused scan and routing tests

Run:

pnpm --filter @ktx/cli run test -- src/scan.test.ts src/index.test.ts

Expected: PASS.

  • Step 8: Commit scan runtime policy wiring

Run:

git add packages/cli/src/scan.ts packages/cli/src/scan.test.ts packages/cli/src/commands/scan-commands.ts packages/cli/src/index.test.ts
git commit -m "feat(cli): pass managed daemon options to scan"

Expected: commit succeeds.

Task 5: Pass pull-config options through MCP local ingest

Files:

  • Modify: packages/context/src/ingest/local-ingest.ts

  • Modify: packages/context/src/mcp/local-project-ports.ts

  • Modify: packages/context/src/mcp/local-project-ports.test.ts

  • Test: packages/context/src/mcp/local-project-ports.test.ts

  • Step 1: Write failing MCP pull-config forwarding test

In packages/context/src/mcp/local-project-ports.test.ts, add this test in the local ingest tool describe block, next to the existing local ingest tests:

  it('passes local ingest pull-config options into runLocalIngest', async () => {
    const runLocalIngest = vi.fn(async () => ({
      result: { ok: true },
      report: {
        id: 'report-1',
        runId: 'run-1',
        jobId: 'job-1',
        sourceKey: 'looker',
        connectionId: 'warehouse',
        body: {
          syncId: 'sync-1',
          workUnits: [],
          failedWorkUnits: [],
          diffSummary: { added: 0, modified: 0, deleted: 0, unchanged: 0 },
          provenanceRows: [],
        },
      },
    } as never));
    const ports = createLocalProjectMcpContextPorts(project, {
      localIngest: {
        adapters: [{ source: 'looker', skillNames: [] }],
        pullConfigOptions: {
          looker: {
            daemonBaseUrl: 'http://127.0.0.1:61234',
          },
        },
        runLocalIngest,
      },
    });

    await expect(
      ports.ingest.run({
        adapter: 'looker',
        connectionId: 'warehouse',
        trigger: 'manual_resync',
        config: {},
      }),
    ).resolves.toMatchObject({
      runId: 'run-1',
      jobId: 'job-1',
      reportId: 'report-1',
    });

    expect(runLocalIngest).toHaveBeenCalledWith(
      expect.objectContaining({
        pullConfigOptions: {
          looker: {
            daemonBaseUrl: 'http://127.0.0.1:61234',
          },
        },
      }),
    );
  });
  • Step 2: Run the failing MCP test

Run:

pnpm --filter @ktx/context run test -- src/mcp/local-project-ports.test.ts

Expected: FAIL because LocalIngestMcpOptions does not accept pullConfigOptions, and MCP local ingest does not pass it to runLocalIngest().

  • Step 3: Add pull-config options to MCP local ingest options

In packages/context/src/ingest/local-ingest.ts, update LocalIngestMcpOptions so the Pick<RunLocalIngestOptions, ...> includes 'pullConfigOptions'. The interface must contain this sequence after the edit:

export interface LocalIngestMcpOptions
  extends Pick<
    RunLocalIngestOptions,
    | 'agentRunner'
    | 'llmProvider'
    | 'memoryModel'
    | 'semanticLayerCompute'
    | 'queryExecutor'
    | 'logger'
    | 'pullConfigOptions'
  > {
  adapters?: SourceAdapter[];
  jobIdFactory?: () => string;
  runLocalMetabaseIngest?: (options: RunLocalMetabaseIngestOptions) => Promise<LocalMetabaseFanoutResult>;
}
  • Step 4: Pass pull-config options in MCP local ingest execution

In packages/context/src/mcp/local-project-ports.ts, in the runLocalIngest({ ... }) call, add this field after sourceDir,:

          pullConfigOptions: options.localIngest?.pullConfigOptions,
  • Step 5: Run MCP tests

Run:

pnpm --filter @ktx/context run test -- src/mcp/local-project-ports.test.ts

Expected: PASS.

  • Step 6: Commit MCP pull-config forwarding

Run:

git add packages/context/src/ingest/local-ingest.ts packages/context/src/mcp/local-project-ports.ts packages/context/src/mcp/local-project-ports.test.ts
git commit -m "feat(context): pass MCP ingest pull config options"

Expected: commit succeeds.

Task 6: Wire managed daemon options through MCP serve

Files:

  • Modify: packages/cli/src/serve.ts

  • Modify: packages/cli/src/serve.test.ts

  • Test: packages/cli/src/serve.test.ts

  • Test: packages/cli/src/index.test.ts

  • Step 1: Write failing serve managed daemon wiring test

In packages/cli/src/serve.test.ts, add this test after uses managed semantic compute when MCP semantic compute has no explicit HTTP URL:

  it('passes managed daemon options to MCP local ingest adapters and pull-config options', async () => {
    const project = { projectDir: '/tmp/ktx-project', config: { connections: {} } } as never;
    const adapters = [{ source: 'looker', skillNames: [] }];
    const createIngestAdapters = vi.fn(() => adapters);
    const createContextTools = vi.fn(() => ({ connections: { list: async () => [] } }));
    const managedRuntimeIo = makeManagedRuntimeIo();

    await expect(
      runKtxServeStdio(
        {
          mcp: 'stdio',
          projectDir: '/tmp/ktx-project',
          userId: 'agent',
          semanticCompute: false,
          semanticComputeUrl: undefined,
          databaseIntrospectionUrl: undefined,
          executeQueries: false,
          memoryCapture: false,
          memoryModel: undefined,
          cliVersion: '0.2.0',
          runtimeInstallPolicy: 'auto',
        },
        {
          loadProject: async () => project,
          createContextTools,
          createIngestAdapters,
          managedRuntimeIo: managedRuntimeIo.io,
          createServer: vi.fn(() => ({ connect: vi.fn(async () => undefined) }) as never),
          createTransport: vi.fn(() => ({}) as never),
          stderr: { write: vi.fn() },
        },
      ),
    ).resolves.toBe(0);

    const expectedManagedDaemon = {
      cliVersion: '0.2.0',
      installPolicy: 'auto',
      io: managedRuntimeIo.io,
    };
    expect(createIngestAdapters).toHaveBeenCalledWith(project, {
      managedDaemon: expectedManagedDaemon,
    });
    expect(createContextTools).toHaveBeenCalledWith(
      project,
      expect.objectContaining({
        localIngest: expect.objectContaining({
          adapters,
          pullConfigOptions: {
            managedDaemon: expectedManagedDaemon,
          },
        }),
      }),
    );
  });

Add this assertion to the existing test that passes databaseIntrospectionUrl: 'http://127.0.0.1:8765':

          localIngest: expect.objectContaining({
            pullConfigOptions: {
              databaseIntrospectionUrl: 'http://127.0.0.1:8765',
            },
          }),
  • Step 2: Run the failing serve tests

Run:

pnpm --filter @ktx/cli run test -- src/serve.test.ts

Expected: FAIL because runKtxServeStdio() does not pass managed daemon options or pull-config options into local ingest.

  • Step 3: Add serve managed daemon option helper

In packages/cli/src/serve.ts, add this import after the managed command import:

import type { ManagedPythonCoreDaemonOptions } from './managed-python-http.js';

Add this helper after requiredManagedRuntimeCliVersion():

function managedDaemonOptionsForServe(
  args: KtxServeArgs,
  deps: KtxServeDeps,
): ManagedPythonCoreDaemonOptions | undefined {
  if (args.databaseIntrospectionUrl || !args.cliVersion) {
    return undefined;
  }
  return {
    cliVersion: args.cliVersion,
    installPolicy: args.runtimeInstallPolicy ?? 'prompt',
    io: deps.managedRuntimeIo ?? process,
  };
}
  • Step 4: Pass managed daemon options to serve local ingest

In runKtxServeStdio(), replace this block:

  const createIngestAdapters = deps.createIngestAdapters ?? createKtxCliLocalIngestAdapters;
  const localAdapters = createIngestAdapters(project, {
    databaseIntrospectionUrl: args.databaseIntrospectionUrl,
  });

with:

  const createIngestAdapters = deps.createIngestAdapters ?? createKtxCliLocalIngestAdapters;
  const managedDaemon = managedDaemonOptionsForServe(args, deps);
  const localAdapterOptions = {
    ...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
    ...(managedDaemon ? { managedDaemon } : {}),
  };
  const localAdapters = createIngestAdapters(project, localAdapterOptions);

In the localIngest object, add this field after adapters: localAdapters,:

    pullConfigOptions: localAdapterOptions,
  • Step 5: Run serve and routing tests

Run:

pnpm --filter @ktx/cli run test -- src/serve.test.ts src/index.test.ts

Expected: PASS.

  • Step 6: Commit serve managed daemon wiring

Run:

git add packages/cli/src/serve.ts packages/cli/src/serve.test.ts
git commit -m "feat(cli): pass managed daemon options to serve ingest"

Expected: commit succeeds.

Task 7: Verify managed local ingest daemon integration

Files:

  • Verify: packages/cli/src/managed-python-http.ts

  • Verify: packages/cli/src/local-adapters.ts

  • Verify: packages/cli/src/ingest.ts

  • Verify: packages/cli/src/scan.ts

  • Verify: packages/cli/src/serve.ts

  • Verify: packages/context/src/ingest/local-ingest.ts

  • Verify: packages/context/src/mcp/local-project-ports.ts

  • Step 1: Run focused CLI tests

Run:

pnpm --filter @ktx/cli run test -- src/managed-python-http.test.ts src/ingest.test.ts src/scan.test.ts src/serve.test.ts src/index.test.ts

Expected: PASS.

  • Step 2: Run focused context tests

Run:

pnpm --filter @ktx/context run test -- src/mcp/local-project-ports.test.ts

Expected: PASS.

  • Step 3: Run affected package type checks

Run:

pnpm --filter @ktx/cli run type-check
pnpm --filter @ktx/context run type-check

Expected: both commands PASS.

  • Step 4: Run the broader TypeScript test surface

Run:

pnpm --filter @ktx/cli run test
pnpm --filter @ktx/context run test

Expected: both commands PASS.

  • Step 5: Commit verification-only fixes if needed

If Step 1 through Step 4 require mechanical test expectation or type fixes, run:

git add packages/cli/src packages/context/src
git commit -m "test: verify managed local ingest daemon runtime"

Expected: commit succeeds only when files changed during verification. If no files changed, skip this commit.

Self-review

Spec coverage:

  • The plan uses the managed core runtime and daemon for Python-backed local ingest helper behavior.
  • The plan preserves explicit daemon URLs and environment-variable override behavior.
  • The plan keeps the first-use installation policy aligned with existing --yes, --no-input, and prompt semantics.
  • The plan avoids local embedding dependency installation by requesting only the core runtime feature.

Placeholder scan:

  • No placeholder markers remain in the task steps.
  • Every code-changing step includes the exact code block or replacement to use.

Type consistency:

  • The new managed daemon option type is named ManagedPythonCoreDaemonOptions.
  • CLI runtime policy fields use the existing KtxManagedPythonInstallPolicy type.
  • MCP local ingest reuses the existing DefaultLocalIngestAdaptersOptions through RunLocalIngestOptions['pullConfigOptions'].