ktx/docs/superpowers/plans/2026-05-11-managed-local-embeddings-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

34 KiB

Managed Local Embeddings 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 sentence-transformers embedding setup use the KTX-managed Python runtime and daemon instead of requiring users to start a manual ktx-daemon process.

Architecture: Add one managed local-embedding helper in the CLI that prompts or fails according to the existing runtime install policy, starts the managed daemon with the local-embeddings feature, and returns the daemon URL for health checks. Store a stable managed-runtime marker in ktx.yaml, and teach context embedding config resolution to turn that marker into a daemon URL only when the CLI has provided one through the environment.

Tech Stack: TypeScript, Vitest, Commander, @clack/prompts, KTX managed Python runtime commands, @ktx/llm embedding health checks.


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 the runtime wheel builder, runtime wheel packaging, the kaelio-ktx Python artifact policy entry, and matching artifact tests.
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md is implemented. The worktree contains managed-python-runtime.ts, the runtime command runner, runtime install, status, doctor, and prune command registration, and matching CLI tests.
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md is implemented. The worktree contains managed-python-command.ts, ktx sl query runtime policy flags, schema validation, and matching sl tests.
  • docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md is implemented. The worktree contains managed-python-daemon.ts, daemon state paths in the runtime layout, runtime start, runtime stop, Python /health version metadata, and matching TypeScript and Python tests.

Spec requirements still outside this plan:

  • Public npm package surface rename from @ktx/cli to @kaelio/ktx.
  • Managed runtime usage for non-embedding Python-backed command paths beyond ktx sl query.
  • Release smoke coverage for npx @kaelio/ktx ... invocation modes.

This plan implements the next local-embedding runtime slice:

  • Selecting local embeddings installs only the local-embeddings runtime feature.
  • Local embedding setup starts or reuses the managed HTTP daemon.
  • --yes installs and starts without prompting.
  • --no-input fails with an exact preparation command when the managed local embedding runtime is missing.
  • Project config records a managed local embedding marker instead of a random daemon port.
  • Context embedding resolution only resolves the marker when the CLI provides the active daemon URL.

File structure

  • Modify packages/context/src/llm/local-config.ts: define the managed local embeddings marker and environment variable, and resolve that marker to a runtime daemon URL.
  • Modify packages/context/src/llm/local-config.test.ts: cover marker resolution, missing daemon URL behavior, and provider construction.
  • Modify packages/context/src/llm/index.ts: export the marker constants.
  • Modify packages/context/src/package-exports.test.ts: assert root exports expose the marker constants.
  • Create packages/cli/src/managed-local-embeddings.ts: start or reuse the managed daemon with local-embeddings and build health/project configs.
  • Create packages/cli/src/managed-local-embeddings.test.ts: cover ready, --yes, prompt, and --no-input behavior.
  • Modify packages/cli/src/setup-embeddings.ts: use the managed helper for local embeddings and persist the managed marker.
  • Modify packages/cli/src/setup-embeddings.test.ts: update local embedding setup expectations and no-input failure behavior.
  • Modify packages/cli/src/setup.ts: pass CLI version and runtime install policy into the embeddings step.
  • Modify packages/cli/src/commands/setup-commands.ts: attach package version to setup runs.
  • Modify packages/cli/src/cli-program.ts: attach package version to the bare interactive setup path.
  • Modify packages/cli/src/index.ts: export the managed local embedding helper for tests and programmatic use.
  • Modify packages/cli/src/index.test.ts and packages/cli/src/setup.test.ts: update setup argument expectations for cliVersion.

Task 1: Add managed embedding marker resolution in context

Files:

  • Modify: packages/context/src/llm/local-config.test.ts

  • Modify: packages/context/src/llm/local-config.ts

  • Modify: packages/context/src/llm/index.ts

  • Modify: packages/context/src/package-exports.test.ts

  • Step 1: Write failing marker resolution tests

In packages/context/src/llm/local-config.test.ts, extend the import from ./local-config.js so it includes the new constants:

import {
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
  createLocalKtxEmbeddingProviderFromConfig,
  createLocalKtxLlmProviderFromConfig,
  resolveLocalKtxEmbeddingConfig,
  resolveLocalKtxLlmConfig,
} from './local-config.js';

Add these tests inside describe('local KTX embedding config', () => { ... }) after the existing resolves sentence-transformers config test:

  it('resolves managed sentence-transformers config from the CLI-provided daemon URL', () => {
    const config: KtxProjectEmbeddingConfig = {
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: {
        base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
        pathPrefix: '',
      },
      batchSize: 32,
    };

    expect(
      resolveLocalKtxEmbeddingConfig(config, {
        [MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: 'http://127.0.0.1:61234',
      }),
    ).toEqual({
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: { baseURL: 'http://127.0.0.1:61234', pathPrefix: '' },
      batchSize: 32,
    });
  });

  it('returns null for managed sentence-transformers when no daemon URL is available', () => {
    const config: KtxProjectEmbeddingConfig = {
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: {
        base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
        pathPrefix: '',
      },
    };

    expect(resolveLocalKtxEmbeddingConfig(config, {})).toBeNull();
  });

In packages/context/src/package-exports.test.ts, add these assertions after the existing root.createLocalKtxEmbeddingProviderFromConfig assertion:

    expect(root.MANAGED_SENTENCE_TRANSFORMERS_BASE_URL).toBe('managed:local-embeddings');
    expect(root.MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV).toBe(
      'KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL',
    );
  • Step 2: Run the failing context tests

Run:

pnpm --filter @ktx/context run test -- src/llm/local-config.test.ts src/package-exports.test.ts

Expected: FAIL with missing exports for MANAGED_SENTENCE_TRANSFORMERS_BASE_URL and MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV.

  • Step 3: Implement marker resolution

In packages/context/src/llm/local-config.ts, add these exports after the LocalConfigDeps interface:

export const MANAGED_SENTENCE_TRANSFORMERS_BASE_URL = 'managed:local-embeddings';
export const MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV = 'KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL';

Add this helper before resolveLocalKtxEmbeddingConfig:

function resolveSentenceTransformersBaseUrl(value: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
  if (!value) {
    return undefined;
  }
  if (value === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL) {
    return resolveOptional(`env:${MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV}`, env);
  }
  return value;
}

Replace resolveLocalKtxEmbeddingConfig with this implementation:

export function resolveLocalKtxEmbeddingConfig(
  config: KtxProjectEmbeddingConfig,
  env: NodeJS.ProcessEnv,
): KtxEmbeddingConfig | null {
  if (config.backend === 'none') {
    return null;
  }
  if (config.backend === 'sentence-transformers') {
    const baseURL = resolveSentenceTransformersBaseUrl(config.sentenceTransformers?.base_url, env);
    if (!baseURL) {
      return null;
    }
    return {
      backend: config.backend,
      model: config.model ?? 'all-MiniLM-L6-v2',
      dimensions: config.dimensions,
      sentenceTransformers: {
        baseURL,
        pathPrefix: config.sentenceTransformers?.pathPrefix,
      },
      batchSize: config.batchSize,
    };
  }
  return {
    backend: config.backend,
    model: config.model ?? 'deterministic',
    dimensions: config.dimensions,
    ...(resolvedProviderConfig(config.openai, env) ? { openai: resolvedProviderConfig(config.openai, env) } : {}),
    batchSize: config.batchSize,
  };
}

In packages/context/src/llm/index.ts, add the new constants to the existing export from ./local-config.js:

export {
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
  createLocalKtxEmbeddingProviderFromConfig,
  createLocalKtxLlmProviderFromConfig,
  resolveLocalKtxEmbeddingConfig,
  resolveLocalKtxLlmConfig,
} from './local-config.js';
  • Step 4: Verify the context marker tests pass

Run:

pnpm --filter @ktx/context run test -- src/llm/local-config.test.ts src/package-exports.test.ts

Expected: PASS.

  • Step 5: Commit

Run:

git add packages/context/src/llm/local-config.ts packages/context/src/llm/local-config.test.ts packages/context/src/llm/index.ts packages/context/src/package-exports.test.ts
git commit -m "feat: add managed local embeddings config marker"

Task 2: Add the managed local embeddings CLI helper

Files:

  • Create: packages/cli/src/managed-local-embeddings.test.ts

  • Create: packages/cli/src/managed-local-embeddings.ts

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

  • Step 1: Write the failing helper tests

Create packages/cli/src/managed-local-embeddings.test.ts with this content:

import { describe, expect, it, vi } from 'vitest';
import {
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
} from '@ktx/context';
import {
  ensureManagedLocalEmbeddingsDaemon,
  managedLocalEmbeddingHealthConfig,
  managedLocalEmbeddingProjectConfig,
} from './managed-local-embeddings.js';
import type { ManagedPythonCommandRuntime } from './managed-python-command.js';
import type { ManagedPythonDaemonStartResult } from './managed-python-daemon.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 runtime(): ManagedPythonCommandRuntime {
  return {
    layout: {
      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',
      daemonStatePath: '/runtime/0.2.0/daemon.json',
      daemonStdoutPath: '/runtime/0.2.0/daemon.stdout.log',
      daemonStderrPath: '/runtime/0.2.0/daemon.stderr.log',
    },
    manifest: {
      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: ['core', 'local-embeddings'],
      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 daemonResult(status: 'started' | 'reused' = 'reused'): ManagedPythonDaemonStartResult {
  return {
    status,
    layout: runtime().layout,
    baseUrl: 'http://127.0.0.1:61234',
    state: {
      schemaVersion: 1,
      pid: 12345,
      host: '127.0.0.1',
      port: 61234,
      version: '0.2.0',
      features: ['core', 'local-embeddings'],
      startedAt: '2026-05-11T00:00:00.000Z',
      stdoutLog: '/runtime/0.2.0/daemon.stdout.log',
      stderrLog: '/runtime/0.2.0/daemon.stderr.log',
    },
  };
}

describe('managedLocalEmbeddingProjectConfig', () => {
  it('uses a stable managed runtime marker instead of a random daemon port', () => {
    expect(
      managedLocalEmbeddingProjectConfig({
        model: 'all-MiniLM-L6-v2',
        dimensions: 384,
      }),
    ).toEqual({
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: {
        base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
        pathPrefix: '',
      },
    });
  });
});

describe('managedLocalEmbeddingHealthConfig', () => {
  it('uses the active managed daemon URL for the immediate health check', () => {
    expect(
      managedLocalEmbeddingHealthConfig({
        baseUrl: 'http://127.0.0.1:61234',
        model: 'all-MiniLM-L6-v2',
        dimensions: 384,
      }),
    ).toEqual({
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: { baseURL: 'http://127.0.0.1:61234', pathPrefix: '' },
    });
  });
});

describe('ensureManagedLocalEmbeddingsDaemon', () => {
  it('ensures the local-embeddings feature and starts the managed daemon', async () => {
    const io = makeIo();
    const ensureRuntime = vi.fn(async () => runtime());
    const startDaemon = vi.fn(async () => daemonResult('started'));

    await expect(
      ensureManagedLocalEmbeddingsDaemon({
        cliVersion: '0.2.0',
        installPolicy: 'auto',
        io: io.io,
        ensureRuntime,
        startDaemon,
      }),
    ).resolves.toEqual({
      baseUrl: 'http://127.0.0.1:61234',
      env: {
        [MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: 'http://127.0.0.1:61234',
      },
    });

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

  it('reuses an already running daemon without reporting a new start', async () => {
    const io = makeIo();

    await ensureManagedLocalEmbeddingsDaemon({
      cliVersion: '0.2.0',
      installPolicy: 'prompt',
      io: io.io,
      ensureRuntime: vi.fn(async () => runtime()),
      startDaemon: vi.fn(async () => daemonResult('reused')),
    });

    expect(io.stderr()).toContain('Using KTX local embeddings daemon: http://127.0.0.1:61234');
  });
});
  • Step 2: Run the failing helper tests

Run:

pnpm --filter @ktx/cli run test -- src/managed-local-embeddings.test.ts

Expected: FAIL with an import error for ./managed-local-embeddings.js.

  • Step 3: Implement the helper

Create packages/cli/src/managed-local-embeddings.ts with this content:

import {
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
  MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV,
} from '@ktx/context';
import type { KtxProjectEmbeddingConfig } from '@ktx/context/project';
import type { KtxEmbeddingConfig } from '@ktx/llm';
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 interface ManagedLocalEmbeddingsDaemon {
  baseUrl: string;
  env: Record<typeof MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV, string>;
}

export interface ManagedLocalEmbeddingsOptions {
  cliVersion: string;
  installPolicy: KtxManagedPythonInstallPolicy;
  io: KtxCliIo;
  ensureRuntime?: (options: {
    cliVersion: string;
    installPolicy: KtxManagedPythonInstallPolicy;
    io: KtxCliIo;
    feature: 'local-embeddings';
  }) => Promise<ManagedPythonCommandRuntime>;
  startDaemon?: (options: {
    cliVersion: string;
    features: ['local-embeddings'];
    force: boolean;
  }) => Promise<ManagedPythonDaemonStartResult>;
}

export function managedLocalEmbeddingProjectConfig(input: {
  model: string;
  dimensions: number;
}): KtxProjectEmbeddingConfig {
  return {
    backend: 'sentence-transformers',
    model: input.model,
    dimensions: input.dimensions,
    sentenceTransformers: {
      base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
      pathPrefix: '',
    },
  };
}

export function managedLocalEmbeddingHealthConfig(input: {
  baseUrl: string;
  model: string;
  dimensions: number;
}): KtxEmbeddingConfig {
  return {
    backend: 'sentence-transformers',
    model: input.model,
    dimensions: input.dimensions,
    sentenceTransformers: {
      baseURL: input.baseUrl,
      pathPrefix: '',
    },
  };
}

export async function ensureManagedLocalEmbeddingsDaemon(
  options: ManagedLocalEmbeddingsOptions,
): Promise<ManagedLocalEmbeddingsDaemon> {
  const ensureRuntime = options.ensureRuntime ?? ensureManagedPythonCommandRuntime;
  const startDaemon = options.startDaemon ?? startManagedPythonDaemon;

  await ensureRuntime({
    cliVersion: options.cliVersion,
    installPolicy: options.installPolicy,
    io: options.io,
    feature: 'local-embeddings',
  });
  const daemon = await startDaemon({
    cliVersion: options.cliVersion,
    features: ['local-embeddings'],
    force: false,
  });

  const verb = daemon.status === 'started' ? 'Started' : 'Using';
  options.io.stderr.write(`${verb} KTX local embeddings daemon: ${daemon.baseUrl}\n`);

  return {
    baseUrl: daemon.baseUrl,
    env: {
      [MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: daemon.baseUrl,
    },
  };
}

In packages/cli/src/index.ts, add this export after the existing managed-python-daemon.js exports:

export {
  ensureManagedLocalEmbeddingsDaemon,
  managedLocalEmbeddingHealthConfig,
  managedLocalEmbeddingProjectConfig,
  type ManagedLocalEmbeddingsDaemon,
  type ManagedLocalEmbeddingsOptions,
} from './managed-local-embeddings.js';
  • Step 4: Verify helper tests pass

Run:

pnpm --filter @ktx/cli run test -- src/managed-local-embeddings.test.ts

Expected: PASS.

  • Step 5: Commit

Run:

git add packages/cli/src/managed-local-embeddings.ts packages/cli/src/managed-local-embeddings.test.ts packages/cli/src/index.ts
git commit -m "feat: add managed local embeddings daemon helper"

Task 3: Wire setup embeddings to the managed runtime

Files:

  • Modify: packages/cli/src/setup-embeddings.ts

  • Modify: packages/cli/src/setup-embeddings.test.ts

  • Step 1: Write failing setup tests for managed local embeddings

In packages/cli/src/setup-embeddings.test.ts, update the import from ./setup-embeddings.js so it also imports the managed install policy type:

import {
  type KtxSetupEmbeddingsPromptAdapter,
  runKtxSetupEmbeddingsStep,
} from './setup-embeddings.js';

Add this helper near makePromptAdapter:

function managedDaemon(baseUrl = 'http://127.0.0.1:61234') {
  return {
    baseUrl,
    env: {
      KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: baseUrl,
    },
  };
}

In every runKtxSetupEmbeddingsStep call that does not inject an embeddingBackend: 'openai', add these arguments:

        cliVersion: '0.2.0',
        runtimeInstallPolicy: 'auto',

In the test named configures local sentence-transformers embeddings after interactive selection, add this dependency:

    const ensureLocalEmbeddings = vi.fn(async () => managedDaemon());

Pass it in the deps object:

      { prompts, env: {}, healthCheck, ensureLocalEmbeddings },

Replace the expected health check config in that test with:

    expect(ensureLocalEmbeddings).toHaveBeenCalledWith({
      cliVersion: '0.2.0',
      installPolicy: 'auto',
      io: io.io,
    });
    expect(healthCheck).toHaveBeenCalledWith({
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: { baseURL: 'http://127.0.0.1:61234', pathPrefix: '' },
    });

Replace the persisted local embedding expectation in that test with:

    expect(config.ingest.embeddings).toMatchObject({
      backend: 'sentence-transformers',
      model: 'all-MiniLM-L6-v2',
      dimensions: 384,
      sentenceTransformers: { base_url: 'managed:local-embeddings', pathPrefix: '' },
    });

Add this new test after the existing non-interactive local embeddings test:

  it('fails non-interactive local setup when the managed local embeddings runtime is missing', async () => {
    const io = makeIo();
    const ensureLocalEmbeddings = vi.fn(async () => {
      throw new Error(
        'KTX Python runtime is required for this command. Run: ktx runtime install --feature local-embeddings --yes',
      );
    });

    const result = await runKtxSetupEmbeddingsStep(
      {
        projectDir: tempDir,
        inputMode: 'disabled',
        cliVersion: '0.2.0',
        runtimeInstallPolicy: 'never',
        skipEmbeddings: false,
      },
      io.io,
      { env: {}, ensureLocalEmbeddings },
    );

    expect(result.status).toBe('failed');
    expect(io.stderr()).toContain(
      'KTX Python runtime is required for this command. Run: ktx runtime install --feature local-embeddings --yes',
    );
  });
  • Step 2: Run the failing setup tests

Run:

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

Expected: FAIL because KtxSetupEmbeddingsArgs has no cliVersion or runtimeInstallPolicy, and KtxSetupEmbeddingsDeps has no ensureLocalEmbeddings.

  • Step 3: Update setup embeddings types and imports

In packages/cli/src/setup-embeddings.ts, add these imports:

import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
import {
  ensureManagedLocalEmbeddingsDaemon,
  managedLocalEmbeddingHealthConfig,
  managedLocalEmbeddingProjectConfig,
  type ManagedLocalEmbeddingsDaemon,
} from './managed-local-embeddings.js';

Add these fields to KtxSetupEmbeddingsArgs after inputMode:

  cliVersion: string;
  runtimeInstallPolicy: KtxManagedPythonInstallPolicy;

Add this dependency to KtxSetupEmbeddingsDeps:

  ensureLocalEmbeddings?: (options: {
    cliVersion: string;
    installPolicy: KtxManagedPythonInstallPolicy;
    io: KtxCliIo;
  }) => Promise<ManagedLocalEmbeddingsDaemon>;
  • Step 4: Replace manual local daemon messaging and config

In packages/cli/src/setup-embeddings.ts, remove these constants:

const LOCAL_EMBEDDING_DAEMON_COMMAND = 'ktx-daemon serve-http --host 127.0.0.1 --port 8765';
const LOCAL_EMBEDDING_DAEMON_DEV_COMMAND =
  'cd ktx && source .venv/bin/activate && uv run ktx-daemon serve-http --host 127.0.0.1 --port 8765';

Replace localEmbeddingSetupMessage with:

function localEmbeddingSetupMessage(message: string): string {
  return [
    `Local embedding health check failed: ${message}`,
    'Local embeddings use the KTX-managed Python runtime.',
    'Prepare the runtime with: ktx runtime start --feature local-embeddings',
    'Use --yes with setup to install and start the runtime without prompting.',
    'The first run may download Python packages and the all-MiniLM-L6-v2 model.',
  ].join('\n');
}

Inside runKtxSetupEmbeddingsStep, before building healthConfig, add this block after the OpenAI credential block:

    let managedLocalEmbeddings: ManagedLocalEmbeddingsDaemon | undefined;
    if (selectedBackend === LOCAL_EMBEDDING_BACKEND) {
      const ensureLocalEmbeddings = deps.ensureLocalEmbeddings ?? ensureManagedLocalEmbeddingsDaemon;
      try {
        managedLocalEmbeddings = await ensureLocalEmbeddings({
          cliVersion: args.cliVersion,
          installPolicy: args.runtimeInstallPolicy,
          io,
        });
      } catch (error) {
        io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
        return { status: 'failed', projectDir: args.projectDir };
      }
    }

Replace the healthConfig assignment with:

    const healthConfig =
      selectedBackend === LOCAL_EMBEDDING_BACKEND && managedLocalEmbeddings
        ? managedLocalEmbeddingHealthConfig({
            baseUrl: managedLocalEmbeddings.baseUrl,
            model,
            dimensions,
          })
        : buildHealthConfig({
            backend: selectedBackend,
            model,
            dimensions,
            credentialValue,
          });

Replace the successful local persistence call inside if (health.ok) { ... } with:

      await persistEmbeddingConfig(
        args.projectDir,
        selectedBackend === LOCAL_EMBEDDING_BACKEND
          ? managedLocalEmbeddingProjectConfig({ model, dimensions })
          : buildProjectEmbeddingConfig({
              backend: selectedBackend,
              model,
              dimensions,
              credentialRef,
            }),
      );
  • Step 5: Verify setup embeddings tests pass

Run:

pnpm --filter @ktx/cli run test -- src/setup-embeddings.test.ts src/managed-local-embeddings.test.ts

Expected: PASS.

  • Step 6: Commit

Run:

git add packages/cli/src/setup-embeddings.ts packages/cli/src/setup-embeddings.test.ts
git commit -m "feat: use managed runtime for local embedding setup"

Task 4: Pass runtime policy and CLI version through setup commands

Files:

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

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

  • Modify: packages/cli/src/cli-program.ts

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

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

  • Step 1: Write failing setup argument expectations

In packages/cli/src/index.test.ts, find the test that routes the main setup command and add cliVersion: '0.0.0-private' to the expected setup run argument object.

Add this assertion to the same test when --yes is present:

        yes: true,
        cliVersion: '0.0.0-private',

In packages/cli/src/setup.test.ts, find the setup test that asserts the embeddings runner arguments. Add these expected fields to the embeddings step argument object:

            cliVersion: '0.2.0',
            runtimeInstallPolicy: 'auto',

Add one focused unit test near the other setup flow tests:

  it('passes no-input runtime policy to the embeddings step', async () => {
    const io = makeIo();
    const embeddings = vi.fn(async () => ({ status: 'failed' as const, projectDir: tempDir }));

    await expect(
      runKtxSetup(
        {
          command: 'run',
          projectDir: tempDir,
          mode: 'existing',
          agents: false,
          agentScope: 'project',
          agentInstallMode: 'cli',
          skipAgents: true,
          inputMode: 'disabled',
          yes: false,
          cliVersion: '0.2.0',
          skipLlm: true,
          skipEmbeddings: false,
          databaseSchemas: [],
          skipDatabases: true,
          skipSources: true,
        },
        io.io,
        {
          project: {
            run: vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir })),
          },
          embeddings,
        },
      ),
    ).resolves.toBe(1);

    expect(embeddings).toHaveBeenCalledWith(
      expect.objectContaining({
        cliVersion: '0.2.0',
        runtimeInstallPolicy: 'never',
      }),
      io.io,
    );
  });
  • Step 2: Run the failing setup routing tests

Run:

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

Expected: FAIL because setup args do not carry cliVersion yet and embeddings args do not derive runtimeInstallPolicy.

  • Step 3: Add cliVersion to setup run args

In packages/cli/src/setup.ts, add this field to the run variant of KtxSetupArgs immediately after yes:

      cliVersion: string;

Add this helper near the other setup helpers:

function setupRuntimeInstallPolicy(args: Extract<KtxSetupArgs, { command: 'run' }>): 'prompt' | 'auto' | 'never' {
  if (args.yes) {
    return 'auto';
  }
  return args.inputMode === 'disabled' ? 'never' : 'prompt';
}

In the embeddings step call inside runKtxSetupInner, add:

            cliVersion: args.cliVersion,
            runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
  • Step 4: Pass package version from Commander and bare setup

In packages/cli/src/commands/setup-commands.ts, add this field to the setup run argument object:

      cliVersion: context.packageInfo.version,

Place it immediately after yes: options.yes === true.

In packages/cli/src/cli-program.ts, add this field to the bare interactive setup argument object inside runBareInteractiveCommand:

        cliVersion: context.packageInfo.version,

Place it immediately after yes: false.

  • Step 5: Verify setup routing tests pass

Run:

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

Expected: PASS.

  • Step 6: Commit

Run:

git add packages/cli/src/setup.ts packages/cli/src/commands/setup-commands.ts packages/cli/src/cli-program.ts packages/cli/src/setup.test.ts packages/cli/src/index.test.ts
git commit -m "feat: pass managed runtime policy through setup"

Task 5: Final verification

Files:

  • Verify: packages/context/src/llm/local-config.ts

  • Verify: packages/cli/src/managed-local-embeddings.ts

  • Verify: packages/cli/src/setup-embeddings.ts

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

  • Step 1: Run focused context tests

Run:

pnpm --filter @ktx/context run test -- src/llm/local-config.test.ts src/package-exports.test.ts

Expected: PASS.

  • Step 2: Run focused CLI tests

Run:

pnpm --filter @ktx/cli run test -- src/managed-local-embeddings.test.ts src/setup-embeddings.test.ts src/setup.test.ts src/index.test.ts

Expected: PASS.

  • Step 3: Run TypeScript checks for changed packages

Run:

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

Expected: PASS.

  • Step 4: Run package-level tests if type-check changed public exports

Run:

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

Expected: PASS.

  • Step 5: Run pre-commit for changed files

Run:

uv run pre-commit run --files packages/context/src/llm/local-config.ts packages/context/src/llm/local-config.test.ts packages/context/src/llm/index.ts packages/context/src/package-exports.test.ts packages/cli/src/managed-local-embeddings.ts packages/cli/src/managed-local-embeddings.test.ts packages/cli/src/setup-embeddings.ts packages/cli/src/setup-embeddings.test.ts packages/cli/src/setup.ts packages/cli/src/commands/setup-commands.ts packages/cli/src/cli-program.ts packages/cli/src/setup.test.ts packages/cli/src/index.test.ts packages/cli/src/index.ts

Expected: PASS. If pre-commit is unavailable because local hook versions are missing, run the focused tests and type-check commands from steps 1 through 3 and record the pre-commit error.

  • Step 6: Commit final verification adjustments

Run this only if final verification required small fixes:

git add packages/context/src/llm/local-config.ts packages/context/src/llm/local-config.test.ts packages/context/src/llm/index.ts packages/context/src/package-exports.test.ts packages/cli/src/managed-local-embeddings.ts packages/cli/src/managed-local-embeddings.test.ts packages/cli/src/setup-embeddings.ts packages/cli/src/setup-embeddings.test.ts packages/cli/src/setup.ts packages/cli/src/commands/setup-commands.ts packages/cli/src/cli-program.ts packages/cli/src/setup.test.ts packages/cli/src/index.test.ts packages/cli/src/index.ts
git commit -m "test: verify managed local embeddings runtime setup"

Acceptance criteria

  • ktx setup --embedding-backend sentence-transformers --yes installs the local-embeddings runtime feature when needed, starts or reuses the managed daemon, probes the active daemon URL, and writes managed:local-embeddings to ktx.yaml.
  • ktx setup --embedding-backend sentence-transformers --no-input fails with the exact runtime preparation command when the runtime is missing.
  • Existing OpenAI embedding setup behavior is unchanged.
  • The project config no longer stores the daemon's random port.
  • resolveLocalKtxEmbeddingConfig returns a usable KtxEmbeddingConfig for managed local embeddings only when KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL is present.
  • Focused CLI and context tests pass.

Self-review

  • Spec coverage: This plan covers lazy local-embeddings installation after local embeddings are selected, separate prompt/no-input behavior, and managed daemon reuse for local embedding setup health checks.
  • Placeholder scan: This plan contains concrete file paths, code snippets, commands, expected outcomes, and commit commands.
  • Type consistency: The new ManagedLocalEmbeddingsDaemon type, managed marker constants, setup argument fields, and helper function names are used consistently across tasks.