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
This commit is contained in:
Andrey Avtomonov 2026-05-11 15:50:34 +02:00 committed by GitHub
parent 075764fe77
commit 9dad936ac7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
99 changed files with 25375 additions and 1538 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,856 @@
# Managed Local Embeddings Release Smoke 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:** Add an opt-in release smoke that proves the public `@kaelio/ktx`
package can install `local-embeddings`, start the managed daemon, compute a real
local embedding, and persist the managed embedding marker through setup.
**Architecture:** Keep the default `artifacts:verify` path lightweight. Add a
separate Node smoke script with an explicit opt-in gate, source-level tests, and
a package script that a release job can run only when large Python and model
downloads are acceptable.
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, uv, KTX managed Python
runtime assets, FastAPI embedding endpoint, sentence-transformers.
---
## 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`
Implementation evidence found before writing this plan includes:
- `scripts/build-python-runtime-wheel.mjs` and matching tests.
- `packages/cli/src/managed-python-runtime.ts`, `runtime.ts`, and
`commands/runtime-commands.ts`.
- `packages/cli/src/managed-python-command.ts` and `ktx sl query` runtime
install policy flags.
- `packages/cli/src/managed-python-daemon.ts` and `ktx runtime start` /
`ktx runtime stop`.
- `packages/cli/src/managed-local-embeddings.ts`,
`packages/context/src/llm/local-config.ts`, and setup embedding wiring.
- `scripts/build-public-npm-package.mjs`, `release-policy.json` listing
`@kaelio/ktx`, and public-package smoke command construction.
- `scripts/package-artifacts.mjs` installed CLI smoke that isolates
`KTX_RUNTIME_ROOT`, lazily installs the core runtime, runs `ktx sl query`,
checks runtime status and doctor output, and starts, reuses, and stops the
core daemon.
The remaining spec gap is the release-check item that permits local embeddings
coverage in a separate job or opt-in check. The default release artifact smoke
must not download `sentence-transformers`, `torch`, or the
`all-MiniLM-L6-v2` model.
## File structure
- Create `scripts/local-embeddings-runtime-smoke.mjs`: an opt-in smoke script
that consumes the built public npm tarball, installs it in a temporary pnpm
project, isolates all runtime and model caches, installs the
`local-embeddings` feature, starts the managed daemon, computes one real
embedding, runs setup with local embeddings, verifies the managed config
marker, and stops the daemon.
- Create `scripts/local-embeddings-runtime-smoke.test.mjs`: fast source-level
tests for opt-in gating, public tarball selection, cache isolation, command
construction, daemon URL parsing, embedding response validation, and package
script registration.
- Modify `package.json`: add `release:local-embeddings-smoke` without adding
it to default `check`, `test`, `artifacts:verify`, or release readiness.
### Task 1: Add failing local embeddings smoke tests
**Files:**
- Create: `scripts/local-embeddings-runtime-smoke.test.mjs`
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
- [ ] **Step 1: Write the failing test file**
Create `scripts/local-embeddings-runtime-smoke.test.mjs` with this content:
```javascript
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { describe, it } from 'node:test';
import {
buildLocalEmbeddingsSmokeEnv,
localEmbeddingsSmokeCommands,
localEmbeddingsSmokeOptIn,
parseDaemonBaseUrl,
publicKtxTarballName,
validateEmbeddingResponse,
} from './local-embeddings-runtime-smoke.mjs';
describe('localEmbeddingsSmokeOptIn', () => {
it('skips unless the smoke is explicitly enabled', () => {
assert.deepEqual(localEmbeddingsSmokeOptIn({}, []), {
run: false,
message: 'Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.',
});
});
it('runs when the environment opt-in is set', () => {
assert.deepEqual(localEmbeddingsSmokeOptIn({ KTX_RUN_LOCAL_EMBEDDINGS_SMOKE: '1' }, []), {
run: true,
});
});
it('runs when --force is present', () => {
assert.deepEqual(localEmbeddingsSmokeOptIn({}, ['--force']), {
run: true,
});
});
});
describe('publicKtxTarballName', () => {
it('selects the public @kaelio/ktx tarball name', () => {
assert.equal(
publicKtxTarballName(['kaelio-ktx-0.0.0-private.tgz', 'ignore-me.tgz']),
'kaelio-ktx-0.0.0-private.tgz',
);
});
it('fails when the public package tarball is missing', () => {
assert.throws(
() => publicKtxTarballName(['ktx-cli-0.0.0-private.tgz']),
/Expected exactly one @kaelio\/ktx tarball/,
);
});
it('fails when multiple public package tarballs are present', () => {
assert.throws(
() => publicKtxTarballName(['kaelio-ktx-0.1.0.tgz', 'kaelio-ktx-0.2.0.tgz']),
/Expected exactly one @kaelio\/ktx tarball/,
);
});
});
describe('buildLocalEmbeddingsSmokeEnv', () => {
it('isolates the runtime root and model caches inside the smoke root', () => {
const env = buildLocalEmbeddingsSmokeEnv('/tmp/ktx-local-embedding-smoke', {
PATH: '/usr/bin',
});
assert.equal(env.PATH, '/usr/bin');
assert.equal(env.KTX_RUN_LOCAL_EMBEDDINGS_SMOKE, '1');
assert.equal(env.KTX_RUNTIME_ROOT, '/tmp/ktx-local-embedding-smoke/managed-runtime');
assert.equal(env.HF_HOME, '/tmp/ktx-local-embedding-smoke/hf-home');
assert.equal(env.TRANSFORMERS_CACHE, '/tmp/ktx-local-embedding-smoke/transformers-cache');
assert.equal(env.SENTENCE_TRANSFORMERS_HOME, '/tmp/ktx-local-embedding-smoke/sentence-transformers-home');
assert.equal(env.TORCH_HOME, '/tmp/ktx-local-embedding-smoke/torch-home');
});
});
describe('localEmbeddingsSmokeCommands', () => {
it('describes the installed-package commands needed for the smoke', () => {
const commands = localEmbeddingsSmokeCommands({
projectDir: '/tmp/ktx-local-embedding-smoke/project',
});
assert.deepEqual(commands.map((command) => command.label), [
'ktx public package version',
'ktx runtime status missing',
'ktx runtime install local embeddings',
'ktx runtime status local embeddings ready',
'ktx runtime start local embeddings',
'ktx setup local embeddings',
'ktx runtime stop local embeddings',
]);
assert.deepEqual(commands[2], {
label: 'ktx runtime install local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
timeoutMs: 1_200_000,
});
assert.deepEqual(commands[4], {
label: 'ktx runtime start local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'start', '--feature', 'local-embeddings'],
timeoutMs: 300_000,
});
assert.deepEqual(commands[5].args, [
'exec',
'ktx',
'setup',
'--project-dir',
'/tmp/ktx-local-embedding-smoke/project',
'--new',
'--no-input',
'--yes',
'--skip-llm',
'--embedding-backend',
'sentence-transformers',
'--skip-databases',
'--skip-sources',
'--skip-agents',
]);
});
});
describe('parseDaemonBaseUrl', () => {
it('extracts the daemon URL from runtime start output', () => {
assert.equal(
parseDaemonBaseUrl('Started KTX Python daemon\nurl: http://127.0.0.1:61234\nfeatures: local-embeddings\n'),
'http://127.0.0.1:61234',
);
});
it('rejects output without a daemon URL', () => {
assert.throws(() => parseDaemonBaseUrl('Started KTX Python daemon\n'), /Daemon URL was not printed/);
});
});
describe('validateEmbeddingResponse', () => {
it('accepts a finite embedding vector with the expected dimensions', () => {
validateEmbeddingResponse({ embedding: [0.1, -0.2, 0.3] }, 3);
});
it('rejects a vector with the wrong dimensions', () => {
assert.throws(
() => validateEmbeddingResponse({ embedding: [0.1, 0.2] }, 3),
/Expected embedding dimension 3, got 2/,
);
});
it('rejects non-finite embedding values', () => {
assert.throws(
() => validateEmbeddingResponse({ embedding: [0.1, Number.NaN, 0.3] }, 3),
/Embedding value at index 1 is not a finite number/,
);
});
});
describe('package script', () => {
it('registers the opt-in local embeddings smoke command', async () => {
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
assert.equal(
packageJson.scripts['release:local-embeddings-smoke'],
'node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in',
);
});
});
```
- [ ] **Step 2: Run the failing test**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs
```
Expected: FAIL with an import error for
`./local-embeddings-runtime-smoke.mjs`.
- [ ] **Step 3: Commit the failing tests**
Run:
```bash
git add scripts/local-embeddings-runtime-smoke.test.mjs
git commit -m "test: specify local embeddings release smoke"
```
### Task 2: Implement the opt-in smoke script
**Files:**
- Create: `scripts/local-embeddings-runtime-smoke.mjs`
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
- [ ] **Step 1: Create the smoke script**
Create `scripts/local-embeddings-runtime-smoke.mjs` with this content:
```javascript
import { execFile } from 'node:child_process';
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const DEFAULT_ROOT_DIR = resolve(SCRIPT_DIR, '..');
const PUBLIC_NPM_ARTIFACT_DIR = join('dist', 'artifacts', 'npm');
const OPT_IN_MESSAGE =
'Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.';
export function localEmbeddingsSmokeOptIn(env = process.env, args = process.argv.slice(2)) {
if (env.KTX_RUN_LOCAL_EMBEDDINGS_SMOKE === '1' || args.includes('--force')) {
return { run: true };
}
return { run: false, message: OPT_IN_MESSAGE };
}
export function publicKtxTarballName(files) {
const matches = files.filter((file) => /^kaelio-ktx-.+\.tgz$/.test(file)).sort();
if (matches.length !== 1) {
throw new Error(
`Expected exactly one @kaelio/ktx tarball in ${PUBLIC_NPM_ARTIFACT_DIR}, found ${matches.length}: ${
matches.join(', ') || 'none'
}. Run pnpm run artifacts:build first.`,
);
}
return matches[0];
}
export async function selectPublicKtxTarball(rootDir = DEFAULT_ROOT_DIR) {
const npmArtifactDir = join(rootDir, PUBLIC_NPM_ARTIFACT_DIR);
const files = await readdir(npmArtifactDir);
return join(npmArtifactDir, publicKtxTarballName(files));
}
export function buildLocalEmbeddingsSmokeEnv(root, baseEnv = process.env) {
return {
...baseEnv,
KTX_RUN_LOCAL_EMBEDDINGS_SMOKE: '1',
KTX_RUNTIME_ROOT: join(root, 'managed-runtime'),
HF_HOME: join(root, 'hf-home'),
TRANSFORMERS_CACHE: join(root, 'transformers-cache'),
SENTENCE_TRANSFORMERS_HOME: join(root, 'sentence-transformers-home'),
TORCH_HOME: join(root, 'torch-home'),
};
}
export function localEmbeddingsSmokeCommands(input) {
return [
{
label: 'ktx public package version',
command: 'pnpm',
args: ['exec', 'ktx', '--version'],
timeoutMs: 60_000,
},
{
label: 'ktx runtime status missing',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'status', '--json'],
timeoutMs: 60_000,
},
{
label: 'ktx runtime install local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
timeoutMs: 1_200_000,
},
{
label: 'ktx runtime status local embeddings ready',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'status', '--json'],
timeoutMs: 60_000,
},
{
label: 'ktx runtime start local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'start', '--feature', 'local-embeddings'],
timeoutMs: 300_000,
},
{
label: 'ktx setup local embeddings',
command: 'pnpm',
args: [
'exec',
'ktx',
'setup',
'--project-dir',
input.projectDir,
'--new',
'--no-input',
'--yes',
'--skip-llm',
'--embedding-backend',
'sentence-transformers',
'--skip-databases',
'--skip-sources',
'--skip-agents',
],
timeoutMs: 900_000,
},
{
label: 'ktx runtime stop local embeddings',
command: 'pnpm',
args: ['exec', 'ktx', 'runtime', 'stop'],
timeoutMs: 60_000,
},
];
}
export function parseDaemonBaseUrl(stdout) {
const match = stdout.match(/^url: (http:\/\/127\.0\.0\.1:\d+)$/m);
if (!match) {
throw new Error(`Daemon URL was not printed by runtime start:\n${stdout}`);
}
return match[1];
}
export function validateEmbeddingResponse(raw, expectedDimensions) {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
throw new Error('Embedding response must be a JSON object');
}
const embedding = raw.embedding;
if (!Array.isArray(embedding)) {
throw new Error('Embedding response must include an embedding array');
}
if (embedding.length !== expectedDimensions) {
throw new Error(`Expected embedding dimension ${expectedDimensions}, got ${embedding.length}`);
}
for (const [index, value] of embedding.entries()) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new Error(`Embedding value at index ${index} is not a finite number`);
}
}
}
async function run(command, args, options = {}) {
process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
try {
const result = await execFileAsync(command, args, {
cwd: options.cwd,
env: { ...process.env, ...options.env },
encoding: 'utf8',
maxBuffer: 1024 * 1024 * 20,
timeout: options.timeoutMs ?? 120_000,
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
return { code: 0, stdout: result.stdout, stderr: result.stderr };
} catch (error) {
const stdout = typeof error.stdout === 'string' ? error.stdout : '';
const stderr = typeof error.stderr === 'string' ? error.stderr : error.message;
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
return {
code: typeof error.code === 'number' ? error.code : 1,
stdout,
stderr,
};
}
}
function requireSuccess(label, result, options = {}) {
if (result.code !== 0) {
throw new Error(`${label} failed with code ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
}
if (options.stderrPattern && !options.stderrPattern.test(result.stderr)) {
throw new Error(`${label} stderr did not match ${options.stderrPattern}\nstderr:\n${result.stderr}`);
}
}
function parseJsonStdout(label, result) {
requireSuccess(label, result);
try {
return JSON.parse(result.stdout);
} catch (error) {
throw new Error(`${label} did not write JSON stdout: ${error.message}\nstdout:\n${result.stdout}`);
}
}
function requireOutput(label, result, pattern) {
if (!pattern.test(result.stdout)) {
throw new Error(`${label} stdout did not match ${pattern}\nstdout:\n${result.stdout}`);
}
}
async function postJson(baseUrl, path, payload, timeoutMs) {
const response = await fetch(new URL(path, baseUrl), {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(timeoutMs),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`POST ${path} failed with ${response.status}: ${text}`);
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(`POST ${path} returned non-JSON response: ${error.message}\n${text}`);
}
}
async function writeSmokePackage(projectDir, tarballPath) {
await mkdir(projectDir, { recursive: true });
await writeFile(
join(projectDir, 'package.json'),
`${JSON.stringify(
{
name: 'ktx-local-embeddings-runtime-smoke',
version: '0.0.0',
private: true,
type: 'module',
dependencies: {
'@kaelio/ktx': `file:${tarballPath}`,
},
},
null,
2,
)}\n`,
);
}
export async function runLocalEmbeddingsRuntimeSmoke(options = {}) {
const rootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
const tarballPath = options.tarballPath ?? (await selectPublicKtxTarball(rootDir));
const root = await mkdtemp(join(tmpdir(), 'ktx-local-embeddings-smoke-'));
const keepTemp = options.keepTemp ?? process.env.KTX_KEEP_LOCAL_EMBEDDINGS_SMOKE === '1';
const installDir = join(root, 'installed-package');
const projectDir = join(root, 'project');
const smokeEnv = buildLocalEmbeddingsSmokeEnv(root);
const commands = localEmbeddingsSmokeCommands({ projectDir });
let daemonStarted = false;
try {
await writeSmokePackage(installDir, tarballPath);
requireSuccess(
'pnpm install public package',
await run('pnpm', ['install', '--ignore-scripts=false'], {
cwd: installDir,
env: smokeEnv,
timeoutMs: 300_000,
}),
);
const version = await run(commands[0].command, commands[0].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[0].timeoutMs,
});
requireSuccess(commands[0].label, version);
requireOutput(commands[0].label, version, /@kaelio\/ktx 0\.0\.0-private/);
const missingStatus = parseJsonStdout(
commands[1].label,
await run(commands[1].command, commands[1].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[1].timeoutMs,
}),
);
if (missingStatus.kind !== 'missing') {
throw new Error(`Expected missing runtime before install, got ${JSON.stringify(missingStatus)}`);
}
const install = await run(commands[2].command, commands[2].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[2].timeoutMs,
});
requireSuccess(commands[2].label, install);
requireOutput(commands[2].label, install, /Installed KTX Python runtime/);
requireOutput(commands[2].label, install, /features: core, local-embeddings/);
const readyStatus = parseJsonStdout(
commands[3].label,
await run(commands[3].command, commands[3].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[3].timeoutMs,
}),
);
if (readyStatus.kind !== 'ready') {
throw new Error(`Expected ready runtime after install, got ${JSON.stringify(readyStatus)}`);
}
if (!readyStatus.manifest?.features?.includes('local-embeddings')) {
throw new Error(`Runtime manifest did not include local-embeddings: ${JSON.stringify(readyStatus.manifest)}`);
}
const start = await run(commands[4].command, commands[4].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[4].timeoutMs,
});
requireSuccess(commands[4].label, start);
daemonStarted = true;
const baseUrl = parseDaemonBaseUrl(start.stdout);
const embeddingResponse = await postJson(
baseUrl,
'/embeddings/compute',
{ text: 'KTX local embeddings release smoke' },
900_000,
);
validateEmbeddingResponse(embeddingResponse, 384);
process.stdout.write('KTX local embeddings daemon computed a 384-dimensional embedding\n');
const setup = await run(commands[5].command, commands[5].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[5].timeoutMs,
});
requireSuccess(commands[5].label, setup);
requireOutput(commands[5].label, setup, /Embeddings ready: yes \(all-MiniLM-L6-v2\)/);
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf8');
if (!config.includes('base_url: managed:local-embeddings')) {
throw new Error(`ktx.yaml did not contain managed local embeddings marker:\n${config}`);
}
process.stdout.write('KTX setup persisted managed local embeddings marker\n');
const stop = await run(commands[6].command, commands[6].args, {
cwd: installDir,
env: smokeEnv,
timeoutMs: commands[6].timeoutMs,
});
requireSuccess(commands[6].label, stop);
daemonStarted = false;
requireOutput(commands[6].label, stop, /Stopped KTX Python daemon/);
process.stdout.write('KTX local embeddings runtime smoke verified\n');
} finally {
if (daemonStarted) {
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop'], {
cwd: installDir,
env: smokeEnv,
timeoutMs: 60_000,
});
}
if (!keepTemp) {
await rm(root, { recursive: true, force: true });
} else {
process.stdout.write(`Kept local embeddings smoke root: ${root}\n`);
}
}
}
async function main() {
const args = process.argv.slice(2);
const optIn = localEmbeddingsSmokeOptIn(process.env, args);
if (!optIn.run) {
process.stdout.write(`Skipping KTX local embeddings runtime smoke. ${optIn.message}\n`);
if (args.includes('--require-opt-in')) {
process.exitCode = 1;
}
return;
}
await runLocalEmbeddingsRuntimeSmoke();
}
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
process.exitCode = 1;
});
}
```
- [ ] **Step 2: Run the smoke test**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs
```
Expected: FAIL only in the package script test because
`release:local-embeddings-smoke` is not registered yet.
- [ ] **Step 3: Commit the smoke script**
Run:
```bash
git add scripts/local-embeddings-runtime-smoke.mjs
git commit -m "feat: add local embeddings runtime smoke"
```
### Task 3: Register the opt-in package script
**Files:**
- Modify: `package.json`
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
- [ ] **Step 1: Add the package script**
In `package.json`, add this script immediately after
`"release:published-smoke"`:
```json
"release:local-embeddings-smoke": "node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in",
```
The surrounding `scripts` section must contain this sequence after the edit:
```json
"release:published-smoke": "node scripts/published-package-smoke.mjs --require-config",
"release:local-embeddings-smoke": "node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in",
"release:readiness": "node scripts/release-readiness.mjs",
```
- [ ] **Step 2: Run the focused test**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs
```
Expected: PASS.
- [ ] **Step 3: Verify the script stays opt-in**
Run:
```bash
pnpm run release:local-embeddings-smoke
```
Expected: FAIL with:
```text
Skipping KTX local embeddings runtime smoke. Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.
```
The command must exit non-zero because `--require-opt-in` is present. This
protects local and CI runs from downloading large dependencies by accident.
- [ ] **Step 4: Commit the package script**
Run:
```bash
git add package.json
git commit -m "chore: register local embeddings smoke"
```
### Task 4: Verify the opt-in smoke path
**Files:**
- Verify: `scripts/local-embeddings-runtime-smoke.mjs`
- Verify: `scripts/local-embeddings-runtime-smoke.test.mjs`
- Verify: `package.json`
- [ ] **Step 1: Run fast script tests**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs scripts/package-artifacts.test.mjs
```
Expected: PASS. Existing package artifact tests must still prove that the
default npm artifact smoke does not prepare an external Python environment or
run local embeddings downloads.
- [ ] **Step 2: Build release artifacts for the smoke**
Run:
```bash
pnpm run artifacts:build
```
Expected: PASS and `dist/artifacts/npm/` contains exactly one
`kaelio-ktx-*.tgz` tarball.
- [ ] **Step 3: Run the opt-in local embeddings smoke**
Run this only in an environment where downloading `sentence-transformers`,
`torch`, and `all-MiniLM-L6-v2` is acceptable:
```bash
KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 pnpm run release:local-embeddings-smoke
```
Expected: PASS with output containing:
```text
KTX local embeddings daemon computed a 384-dimensional embedding
KTX setup persisted managed local embeddings marker
KTX local embeddings runtime smoke verified
```
- [ ] **Step 4: Run release readiness**
Run:
```bash
pnpm run release:readiness
```
Expected: PASS. The readiness report must not require
`release:local-embeddings-smoke`; that smoke remains a separately triggered
release job.
- [ ] **Step 5: Run pre-commit for changed files when configured**
Run:
```bash
uv run pre-commit run --files scripts/local-embeddings-runtime-smoke.mjs scripts/local-embeddings-runtime-smoke.test.mjs package.json
```
Expected: PASS. If pre-commit is unavailable in the environment, record the
tooling failure and keep the previous verification output.
- [ ] **Step 6: Commit verification fixes if needed**
If verification required edits, run:
```bash
git add scripts/local-embeddings-runtime-smoke.mjs scripts/local-embeddings-runtime-smoke.test.mjs package.json
git commit -m "fix: verify local embeddings smoke"
```
Skip this commit when no files changed after the previous commits.
## Acceptance criteria
- `node --test scripts/local-embeddings-runtime-smoke.test.mjs` passes.
- `pnpm run release:local-embeddings-smoke` fails fast without the opt-in
environment variable and prints the exact opt-in guidance.
- `KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 pnpm run release:local-embeddings-smoke`
installs the public `@kaelio/ktx` tarball into a clean project, isolates
`KTX_RUNTIME_ROOT` and model caches, installs `local-embeddings`, starts the
managed daemon, computes a 384-dimensional embedding through
`/embeddings/compute`, runs setup with `--embedding-backend
sentence-transformers`, verifies `base_url: managed:local-embeddings` in
`ktx.yaml`, and stops the daemon.
- The default `pnpm run artifacts:verify`, `pnpm run release:readiness`, and
`pnpm run check` paths do not run the local embeddings smoke.
## Self-review
- Spec coverage: this plan covers the remaining release-check item for local
embeddings in a separate job or opt-in check. Earlier implemented plans cover
the bundled wheel, managed runtime installer, `sl query` command integration,
daemon lifecycle, managed local embeddings runtime behavior, public npm
package assembly, and default core runtime release smoke.
- Placeholder scan: no steps contain placeholder implementation language.
- Type consistency: runtime feature names are consistently `core` and
`local-embeddings`; the public npm package name is `@kaelio/ktx`; the opt-in
environment variable is `KTX_RUN_LOCAL_EMBEDDINGS_SMOKE`; the managed local
embedding marker remains `managed:local-embeddings`.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,239 @@
# Managed Local Embeddings Smoke Public Version 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 the opt-in local embeddings release smoke validate the public
`@kaelio/ktx` package version instead of the private workspace version.
**Architecture:** Reuse the public package constants from
`scripts/build-public-npm-package.mjs` inside the local embeddings smoke. Add a
small exported RegExp helper so the unit test can lock the version expectation
without running the expensive model-download smoke.
**Tech Stack:** Node.js ESM scripts, `node:test`, pnpm release scripts.
---
## Current State
The npm-managed Python runtime spec is
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
The current branch already contains implementation commits for each existing
plan derived from that spec.
Implemented spec-derived plans:
- `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`
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-prune-smoke-and-docs.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-uv-prerequisite-contract.md`
- `docs/superpowers/plans/2026-05-11-single-public-runtime-artifact-cleanup.md`
The remaining gap is in
`scripts/local-embeddings-runtime-smoke.mjs`. The script selects and installs a
public tarball named `kaelio-ktx-*.tgz` and writes a smoke package dependency on
`@kaelio/ktx`, but line 267 still expects `@kaelio/ktx 0.0.0-private`. The
public package builder defines `PUBLIC_NPM_PACKAGE_VERSION = '0.1.0'`, and the
main packed-package smoke already expects `@kaelio/ktx 0.1.0`.
## File Structure
This change keeps the release version source of truth in one script and reuses
it from the opt-in smoke.
- Modify `scripts/local-embeddings-runtime-smoke.mjs`: import the public package
constants, export `expectedPublicKtxVersionPattern()`, and use that pattern
for the smoke version assertion.
- Modify `scripts/local-embeddings-runtime-smoke.test.mjs`: import
`expectedPublicKtxVersionPattern()` and assert that it accepts
`@kaelio/ktx 0.1.0` and rejects `@kaelio/ktx 0.0.0-private`.
### Task 1: Align the local embeddings smoke version assertion
**Files:**
- Modify: `scripts/local-embeddings-runtime-smoke.mjs:1-267`
- Modify: `scripts/local-embeddings-runtime-smoke.test.mjs:5-118`
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
- [ ] **Step 1: Write the failing version-pattern test**
In `scripts/local-embeddings-runtime-smoke.test.mjs`, update the import block
to include `expectedPublicKtxVersionPattern`:
```js
import {
buildLocalEmbeddingsSmokeEnv,
expectedPublicKtxVersionPattern,
localEmbeddingsSmokeCommands,
localEmbeddingsSmokeOptIn,
parseDaemonBaseUrl,
publicKtxTarballName,
validateEmbeddingResponse,
} from './local-embeddings-runtime-smoke.mjs';
```
Then add this test after the `publicKtxTarballName` describe block:
```js
describe('expectedPublicKtxVersionPattern', () => {
it('matches the public package version and rejects the private workspace version', () => {
const pattern = expectedPublicKtxVersionPattern();
assert.match('@kaelio/ktx 0.1.0\n', pattern);
assert.doesNotMatch('@kaelio/ktx 0.0.0-private\n', pattern);
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs
```
Expected: FAIL with an ESM export error that says
`expectedPublicKtxVersionPattern` is not exported from
`./local-embeddings-runtime-smoke.mjs`.
- [ ] **Step 3: Import the public package constants**
In `scripts/local-embeddings-runtime-smoke.mjs`, add this import after the
existing Node imports:
```js
import {
PUBLIC_NPM_PACKAGE_NAME,
PUBLIC_NPM_PACKAGE_VERSION,
} from './build-public-npm-package.mjs';
```
The top of the file becomes:
```js
import { execFile } from 'node:child_process';
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import {
PUBLIC_NPM_PACKAGE_NAME,
PUBLIC_NPM_PACKAGE_VERSION,
} from './build-public-npm-package.mjs';
```
- [ ] **Step 4: Add the version-pattern helper**
In `scripts/local-embeddings-runtime-smoke.mjs`, add these functions after the
`OPT_IN_MESSAGE` constant:
```js
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function expectedPublicKtxVersionPattern() {
return new RegExp(
`${escapeRegExp(PUBLIC_NPM_PACKAGE_NAME)} ${escapeRegExp(PUBLIC_NPM_PACKAGE_VERSION)}`,
);
}
```
- [ ] **Step 5: Use the helper in the smoke**
In `scripts/local-embeddings-runtime-smoke.mjs`, replace this line:
```js
requireOutput(commands[0].label, version, /@kaelio\/ktx 0\.0\.0-private/);
```
with:
```js
requireOutput(commands[0].label, version, expectedPublicKtxVersionPattern());
```
- [ ] **Step 6: Run the focused test**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs
```
Expected: PASS. The new test proves the smoke accepts `@kaelio/ktx 0.1.0` and
rejects `@kaelio/ktx 0.0.0-private`.
- [ ] **Step 7: Run related release-script tests**
Run:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs
```
Expected: PASS. These tests cover the public package constants, tarball name,
artifact smoke source, and local embeddings smoke helpers.
- [ ] **Step 8: Run a stale-expectation search**
Run:
```bash
rg -n "@kaelio/ktx 0\\.0\\.0-private|0\\\\\\.0\\\\\\.0-private" scripts/local-embeddings-runtime-smoke.mjs
```
Expected: no output. The opt-in local embeddings smoke no longer contains the
private package version expectation. The test file still uses
`@kaelio/ktx 0.0.0-private` as a negative fixture.
- [ ] **Step 9: Commit**
Run:
```bash
git add scripts/local-embeddings-runtime-smoke.mjs scripts/local-embeddings-runtime-smoke.test.mjs
git commit -m "fix: align local embeddings smoke with public version"
```
## Verification
Run these checks before marking the plan complete:
```bash
node --test scripts/local-embeddings-runtime-smoke.test.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs
rg -n "@kaelio/ktx 0\\.0\\.0-private|0\\\\\\.0\\\\\\.0-private" scripts/local-embeddings-runtime-smoke.mjs
```
Expected results:
- `node --test ...` exits with code 0.
- `rg ...` prints no matches.
- No Python files changed, so the repository Python pre-commit requirement does
not apply.
## Self-Review
- Spec coverage: this plan fixes the opt-in local embeddings release smoke from
the npm-managed runtime spec so it validates the public npm package produced
by the current release artifact flow.
- Placeholder scan: the plan contains concrete file paths, code blocks,
commands, and expected outcomes.
- Type consistency: the helper name is consistently
`expectedPublicKtxVersionPattern`, and it uses
`PUBLIC_NPM_PACKAGE_NAME` plus `PUBLIC_NPM_PACKAGE_VERSION` from the public
package builder.

File diff suppressed because it is too large Load diff

View file

@ -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<ManagedPythonRuntimeStatus>;
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
confirmInstall?: (message: string) => Promise<boolean>;
}
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<boolean> {
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<ManagedPythonCommandRuntime> {
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<KtxSemanticLayerComputePort> {
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<KtxSemanticLayerComputePort>;
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.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,585 @@
# Managed Python Runtime Release Smoke 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 the public `@kaelio/ktx` artifact smoke prove that the npm
package installs and uses its own managed Python runtime without an externally
prepared Python environment.
**Architecture:** Keep the release smoke black-box: install the packed public
npm tarball into a clean project, isolate `KTX_RUNTIME_ROOT`, and exercise the
installed `ktx` binary. The first `ktx sl query --yes` performs the lazy core
runtime install from bundled package assets, then the smoke verifies
`runtime status`, `runtime doctor`, daemon start/reuse, and daemon stop.
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, uv, KTX CLI managed
Python runtime assets.
---
## 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`
- `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`
All six are implemented in this worktree. Evidence found before writing this
plan includes:
- `scripts/build-python-runtime-wheel.mjs` and
`scripts/build-python-runtime-wheel.test.mjs`.
- `packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl` 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 `ktx sl query` runtime
install policy flags.
- `packages/cli/src/managed-python-daemon.ts`, daemon state paths, and
`ktx runtime start` / `ktx runtime stop`.
- `packages/cli/src/managed-local-embeddings.ts`,
`packages/context/src/llm/local-config.ts` managed marker constants, and
setup wiring in `packages/cli/src/setup-embeddings.ts`.
- `scripts/build-public-npm-package.mjs`,
`scripts/build-public-npm-package.test.mjs`, `release-policy.json` listing
`@kaelio/ktx`, and published smoke command construction for the required
`@kaelio/ktx` invocation modes.
The remaining release-smoke gap is in `scripts/package-artifacts.mjs`:
- `verifyNpmArtifacts()` creates a smoke `.venv`, installs the built Python
runtime wheel into it, and runs installed CLI smoke scripts with that venv at
the front of `PATH`.
- The installed CLI smoke does run `ktx sl query --yes`, but it does not
isolate `KTX_RUNTIME_ROOT`, does not assert that the first query installed
the managed runtime from bundled npm assets, and does not exercise
`ktx runtime status`, `doctor`, `start`, reuse, and `stop`.
This plan closes that release-flow gap without changing the separate Python
artifact smoke. `verifyPythonArtifacts()` must continue to install the built
Python wheel directly because it verifies the Python artifact itself.
## File structure
- Modify `scripts/package-artifacts.test.mjs`: remove the npm-smoke venv test,
add a source-level guard that npm artifact verification does not prepare an
external Python venv, and assert that the installed CLI smoke exercises the
managed runtime lifecycle.
- Modify `scripts/package-artifacts.mjs`: remove npm-smoke Python venv PATH
setup, isolate `KTX_RUNTIME_ROOT` inside `npmRuntimeSmokeSource()`, assert
first-run lazy install, and add runtime status/doctor/start/reuse/stop smoke
commands.
### Task 1: Add failing release-smoke tests
**Files:**
- Modify: `scripts/package-artifacts.test.mjs`
- Test: `scripts/package-artifacts.test.mjs`
- [ ] **Step 1: Remove the stale npm-smoke venv import**
In `scripts/package-artifacts.test.mjs`, delete `npmSmokePythonEnv` from the
import list. The surrounding import block must contain this sequence after the
edit:
```javascript
npmDemoSmokeSource,
npmRuntimeSmokeSource,
npmSmokePackageJson,
npmVerifySource,
```
- [ ] **Step 2: Replace the npm-smoke venv test with a source guard**
Delete this entire test block:
```javascript
describe('npmSmokePythonEnv', () => {
it('prepends the npm smoke virtualenv bin directory to PATH', () => {
const env = npmSmokePythonEnv('/tmp/ktx-npm-smoke', { PATH: '/usr/bin' });
assert.match(env.PATH, /^\/tmp\/ktx-npm-smoke\/\.venv\/(bin|Scripts)/);
assert.match(env.PATH, /\/usr\/bin$/);
});
});
```
Insert this block in the same location:
```javascript
describe('verifyNpmArtifacts', () => {
it('does not prepare an external Python environment for the npm smoke', async () => {
const source = await readFile(new URL('./package-artifacts.mjs', import.meta.url), 'utf8');
const start = source.indexOf('async function verifyNpmArtifacts');
const end = source.indexOf('async function verifyNpmDemoArtifacts');
assert.ok(start > 0, 'verifyNpmArtifacts function must exist');
assert.ok(end > start, 'verifyNpmDemoArtifacts must follow verifyNpmArtifacts');
const body = source.slice(start, end);
assert.doesNotMatch(body, /uv', \['venv', '\.venv'\]/);
assert.doesNotMatch(body, /pythonArtifactInstallArgs/);
assert.doesNotMatch(body, /npmSmokePythonEnv/);
});
});
```
- [ ] **Step 3: Extend the installed CLI smoke assertions**
In the `it('runs installed CLI commands through the public package runtime',
...)` test, add these assertions after the existing
`assert.match(source, /ktx sl query sqlite execute/);` assertion:
```javascript
assert.match(source, /import Database from 'better-sqlite3'/);
assert.doesNotMatch(source, /run\('python'/);
assert.match(source, /KTX_RUNTIME_ROOT/);
assert.match(source, /managed-runtime/);
assert.match(source, /ktx runtime status missing/);
assert.match(source, /runtimeStatusBefore\.kind, 'missing'/);
assert.match(source, /Installing KTX Python runtime \(core\) with uv/);
assert.match(source, /KTX Python runtime ready:/);
assert.match(source, /ktx runtime status ready/);
assert.match(source, /runtimeStatusAfter\.kind, 'ready'/);
assert.match(source, /runtimeStatusAfter\.manifest\.features/);
assert.match(source, /ktx runtime doctor/);
assert.match(source, /PASS Managed Python runtime/);
assert.match(source, /ktx runtime start/);
assert.match(source, /ktx runtime start reuse/);
assert.match(source, /Using existing KTX Python daemon/);
assert.match(source, /ktx runtime stop/);
```
- [ ] **Step 4: Run the failing package artifact tests**
Run:
```bash
node --test scripts/package-artifacts.test.mjs
```
Expected: FAIL. The guard fails because `verifyNpmArtifacts()` still creates
the npm-smoke `.venv`, and the installed CLI smoke assertions fail because
`npmRuntimeSmokeSource()` does not yet isolate or verify the managed runtime.
### Task 2: Make the npm smoke use only the managed runtime
**Files:**
- Modify: `scripts/package-artifacts.mjs`
- Modify: `scripts/package-artifacts.test.mjs`
- Test: `scripts/package-artifacts.test.mjs`
- [ ] **Step 1: Remove the npm-smoke PATH helper**
In `scripts/package-artifacts.mjs`, change the path import from:
```javascript
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
```
to:
```javascript
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
```
Then delete this exported helper:
```javascript
export function npmSmokePythonEnv(projectDir, baseEnv = process.env) {
const binDir = process.platform === 'win32' ? join(projectDir, '.venv', 'Scripts') : join(projectDir, '.venv', 'bin');
const existingPath = baseEnv.PATH ?? '';
return Object.assign({}, baseEnv, {
PATH: existingPath ? `${binDir}${delimiter}${existingPath}` : binDir,
});
}
```
- [ ] **Step 2: Add runtime-smoke helpers to `npmRuntimeSmokeSource()`**
Inside the template string returned by `npmRuntimeSmokeSource()`, add this
helper immediately after `requireSuccess()`:
```javascript
function requireSuccessWithStderr(label, result, stderrPattern) {
assert.equal(
result.code,
0,
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
);
assert.match(result.stderr, stderrPattern, label + ' stderr did not match ' + stderrPattern);
}
```
Then replace the smoke root setup:
```javascript
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
try {
const projectDir = join(root, 'project');
const sourceDir = join(root, 'source');
```
with:
```javascript
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
process.env.KTX_RUNTIME_ROOT = join(root, 'managed-runtime');
let daemonStarted = false;
try {
const projectDir = join(root, 'project');
const sourceDir = join(root, 'source');
```
Finally replace the existing `finally` block at the end of
`npmRuntimeSmokeSource()`:
```javascript
} finally {
await rm(root, { recursive: true, force: true });
}
```
with:
```javascript
} finally {
if (daemonStarted) {
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
}
if (previousRuntimeRoot === undefined) {
delete process.env.KTX_RUNTIME_ROOT;
} else {
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
}
await rm(root, { recursive: true, force: true });
}
```
- [ ] **Step 3: Create the sqlite smoke warehouse without Python**
Inside the template string returned by `npmRuntimeSmokeSource()`, add this
import after the `assert` import:
```javascript
import Database from 'better-sqlite3';
```
Then replace the current `writeSqliteWarehouse()` function:
```javascript
async function writeSqliteWarehouse(projectDir) {
const createDb = await run('python', [
'-c',
[
'import sqlite3',
'import sys',
'db_path = sys.argv[1]',
'conn = sqlite3.connect(db_path)',
'conn.executescript("""',
'DROP TABLE IF EXISTS orders;',
'CREATE TABLE orders (',
' id INTEGER PRIMARY KEY,',
' status TEXT NOT NULL,',
' amount INTEGER NOT NULL',
');',
"INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);",
'""")',
'conn.close()',
].join('\\n'),
join(projectDir, 'warehouse.db'),
]);
requireSuccess('create sqlite warehouse', createDb);
}
```
with:
```javascript
async function writeSqliteWarehouse(projectDir) {
const database = new Database(join(projectDir, 'warehouse.db'));
try {
database.exec(`
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL,
amount INTEGER NOT NULL
);
INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);
`);
} finally {
database.close();
}
}
```
- [ ] **Step 4: Assert the isolated runtime is initially missing**
In `npmRuntimeSmokeSource()`, insert this block immediately after the public
package version assertion:
```javascript
const runtimeStatusBefore = parseJsonResult(
'ktx runtime status missing',
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
);
assert.equal(runtimeStatusBefore.kind, 'missing');
assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
process.stdout.write('ktx managed runtime starts missing in isolated root\\n');
```
- [ ] **Step 5: Assert first `sl query --yes` performs lazy managed install**
In `npmRuntimeSmokeSource()`, replace the current `slQuery` verification block:
```javascript
const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
'--connection-id',
'warehouse',
'--measure',
'orders.order_count',
'--format',
'json',
'--yes',
'--project-dir',
projectDir,
]);
requireSuccess('ktx sl query', slQuery);
requireOutput('ktx sl query', slQuery, /"mode": "compile_only"/);
requireOutput('ktx sl query', slQuery, /orders/);
```
with:
```javascript
const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
'--connection-id',
'warehouse',
'--measure',
'orders.order_count',
'--format',
'json',
'--yes',
'--project-dir',
projectDir,
]);
requireSuccessWithStderr(
'ktx sl query first managed runtime install',
slQuery,
/Installing KTX Python runtime \(core\) with uv[\s\S]*KTX Python runtime ready:/,
);
requireOutput('ktx sl query first managed runtime install', slQuery, /"mode": "compile_only"/);
requireOutput('ktx sl query first managed runtime install', slQuery, /orders/);
const runtimeStatusAfter = parseJsonResult(
'ktx runtime status ready',
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
);
assert.equal(runtimeStatusAfter.kind, 'ready');
assert.deepEqual(runtimeStatusAfter.manifest.features, ['core']);
assert.equal(runtimeStatusAfter.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
process.stdout.write('ktx managed runtime lazy install verified\\n');
```
- [ ] **Step 6: Add runtime doctor and daemon lifecycle smoke**
In `npmRuntimeSmokeSource()`, insert this block immediately after the
`sqliteSlQuery` verification block:
```javascript
const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'runtime', 'doctor']);
requireSuccess('ktx runtime doctor', runtimeDoctor);
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS uv/);
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Bundled Python wheel/);
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Managed Python runtime/);
process.stdout.write('ktx runtime doctor verified\\n');
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
requireSuccess('ktx runtime start', runtimeStart);
daemonStarted = true;
requireOutput('ktx runtime start', runtimeStart, /Started KTX Python daemon/);
requireOutput('ktx runtime start', runtimeStart, /url: http:\/\/127\.0\.0\.1:\d+/);
requireOutput('ktx runtime start', runtimeStart, /features: core/);
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
requireSuccess('ktx runtime start reuse', runtimeStartReuse);
requireOutput('ktx runtime start reuse', runtimeStartReuse, /Using existing KTX Python daemon/);
requireOutput('ktx runtime start reuse', runtimeStartReuse, /features: core/);
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
requireSuccess('ktx runtime stop', runtimeStop);
daemonStarted = false;
requireOutput('ktx runtime stop', runtimeStop, /Stopped KTX Python daemon/);
process.stdout.write('ktx runtime daemon lifecycle verified\\n');
```
- [ ] **Step 7: Remove npm-smoke Python preparation from artifact verification**
In `scripts/package-artifacts.mjs`, replace `verifyNpmArtifacts()` with this
implementation:
```javascript
async function verifyNpmArtifacts(layout, tmpRoot) {
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
}
const projectDir = join(tmpRoot, 'npm-clean-install');
await mkdir(projectDir, { recursive: true });
await writeFile(
join(projectDir, 'package.json'),
`${JSON.stringify(npmSmokePackageJson(layout), null, 2)}\n`,
);
await writeFile(join(projectDir, 'verify-npm.mjs'), npmVerifySource());
await writeFile(join(projectDir, 'verify-installed-cli.mjs'), npmRuntimeSmokeSource());
await writeFile(join(projectDir, 'verify-installed-demo.mjs'), npmDemoSmokeSource());
await runCommand('pnpm', ['install'], { cwd: projectDir });
await runCommand('pnpm', ['rebuild', 'better-sqlite3'], { cwd: projectDir });
await runCommand('node', ['verify-npm.mjs'], { cwd: projectDir });
await runCommand('pnpm', ['exec', 'ktx', '--version'], { cwd: projectDir });
await runCommand('node', ['verify-installed-cli.mjs'], { cwd: projectDir });
await runCommand('node', ['verify-installed-demo.mjs'], { cwd: projectDir });
}
```
- [ ] **Step 8: Run the focused package artifact tests**
Run:
```bash
node --test scripts/package-artifacts.test.mjs
```
Expected: PASS.
- [ ] **Step 9: Commit the release-smoke implementation**
Run:
```bash
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
git commit -m "test: verify managed runtime in public package smoke"
```
### Task 3: Verify the release-smoke surface
**Files:**
- Test: `scripts/package-artifacts.test.mjs`
- Test: `scripts/package-artifacts.mjs`
- [ ] **Step 1: Run script unit tests that cover artifact packaging**
Run:
```bash
node --test scripts/build-python-runtime-wheel.test.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs
```
Expected: PASS.
- [ ] **Step 2: Run the public package artifact smoke**
Run:
```bash
pnpm run artifacts:verify
```
Expected: PASS. The `verify-installed-cli.mjs` output must include:
```text
ktx managed runtime starts missing in isolated root
ktx managed runtime lazy install verified
ktx runtime doctor verified
ktx runtime daemon lifecycle verified
```
- [ ] **Step 3: Run release readiness**
Run:
```bash
pnpm run release:readiness
```
Expected: PASS. The report must still list `@kaelio/ktx` as the only npm
package and must still report registry publishing as disabled by
`release-policy.json`.
- [ ] **Step 4: Run pre-commit for changed files**
Run:
```bash
if [ -d .venv ]; then source .venv/bin/activate; fi
uv run pre-commit run --files scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
```
Expected: PASS. If pre-commit cannot run because the local environment lacks a
compatible hook version, record the exact failure and keep the passing
`node --test` and artifact smoke results.
- [ ] **Step 5: Commit verification fixes if needed**
If Step 1, Step 2, Step 3, or Step 4 required edits, run:
```bash
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
git commit -m "test: finalize managed runtime release smoke"
```
If no files changed after Task 2, do not create an empty commit.
## Acceptance criteria
- `verifyNpmArtifacts()` no longer creates a Python `.venv`, no longer calls
`pythonArtifactInstallArgs()`, and no longer runs npm smoke scripts with a
custom Python venv at the front of `PATH`.
- The installed public npm smoke creates its sqlite warehouse with
`better-sqlite3` and does not shell out to `python`.
- The installed public npm smoke sets an isolated `KTX_RUNTIME_ROOT` and
confirms that `ktx runtime status --json` starts as `missing`.
- The first installed `ktx sl query --yes` installs the `core` managed Python
runtime from bundled npm package assets and still returns compile-only SQL.
- A second semantic query executes against sqlite using the installed managed
runtime.
- `ktx runtime doctor` passes after lazy install.
- `ktx runtime start` starts a core daemon, a second `ktx runtime start` reuses
the daemon, and `ktx runtime stop` stops it.
- The separate Python artifact verification still installs and tests the
Python wheel directly.
- Focused script tests, `pnpm run artifacts:verify`, release readiness, and
pre-commit pass or have explicitly recorded environment blockers.
## Self-review
- Spec coverage: the previous six plans cover the bundled wheel, runtime
installer, `sl query` command integration, daemon lifecycle, local embeddings,
and public npm package surface. This plan covers release-flow checks for clean
install of the packed npm package, first-run managed runtime install from the
bundled wheel, one-shot semantic-layer query through the managed runtime,
runtime status and doctor output, and daemon start/reuse/stop.
- Remaining intentional gap: optional `local-embeddings` smoke remains outside
the default release artifact smoke because the spec permits it in a separate
job or opt-in check and the dependency downloads are large.
- Placeholder scan: no steps contain placeholder implementation language.
- Type consistency: runtime feature names remain `core` and
`local-embeddings`; the public npm package name remains `@kaelio/ktx`; the
runtime root environment variable is `KTX_RUNTIME_ROOT`.

View file

@ -0,0 +1,657 @@
# Managed Runtime Docs and Postgres Smoke Cleanup 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:** Remove the remaining manual Python service guidance from the Postgres
historic SQL smoke and update public docs so the npm-managed Python runtime is
the documented path.
**Architecture:** Keep the existing managed-runtime code unchanged. Add source
and docs guards first, then make the Postgres historic smoke use the
CLI-managed core daemon through `createKtxCliLocalIngestAdapters()`, and update
the README files that still describe internal package artifacts, manual
`ktx-daemon` startup, or `python-service/`.
**Tech Stack:** Bash, Node 22 ESM, `node:test`, Markdown, pnpm, uv, KTX CLI
managed Python runtime.
---
## 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`
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-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`
runtime policy flags.
- `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.
- `packages/cli/src/managed-python-http.ts`,
`packages/cli/src/local-adapters.ts`, `packages/cli/src/ingest.ts`,
`packages/cli/src/scan.ts`, and `packages/cli/src/serve.ts` wire local ingest
helper paths to the managed core daemon.
The remaining drift is documentation and one example smoke script:
- `examples/postgres-historic/scripts/smoke.sh` still checks for
`python-service/.venv`, starts `uvicorn app.main:app`, and exports
`KTX_SQL_ANALYSIS_URL`.
- `examples/postgres-historic/README.md` still documents
`python-service/.venv` or `KTX_SQL_ANALYSIS_URL` as a prerequisite.
- `examples/package-artifacts/README.md` still says the npm smoke installs
generated `@ktx/context` and `@ktx/cli` tarballs.
- `README.md` still presents source-tree `pnpm run ktx -- ...` commands as the
quick start and tells users to start `ktx-daemon` manually for MCP.
This plan closes that drift. It does not rename internal workspace packages and
does not remove explicit daemon URL override behavior from production code.
## File structure
- Modify `scripts/examples-docs.test.mjs`: add regression coverage for managed
runtime docs, public npm package docs, and the Postgres smoke script.
- Modify `examples/postgres-historic/scripts/smoke.sh`: remove
`python-service/` startup and pass managed daemon options into stage-only
historic SQL ingest.
- Modify `examples/postgres-historic/README.md`: document the managed runtime
and remove old SQL-analysis service instructions.
- Modify `examples/package-artifacts/README.md`: describe the single public
`@kaelio/ktx` npm artifact and managed runtime smoke.
- Modify `README.md`: make public `@kaelio/ktx` invocation modes and managed
runtime commands visible while keeping source-tree development commands in
the development section.
### Task 1: Add failing docs and smoke guards
**Files:**
- Modify: `scripts/examples-docs.test.mjs`
- Test: `scripts/examples-docs.test.mjs`
- [ ] **Step 1: Add public runtime README assertions**
In `scripts/examples-docs.test.mjs`, insert this test after the existing
`walks through ktx connection list and ktx connection test in the README
quickstart` test:
```javascript
it('documents public npm and managed runtime usage in the README', async () => {
const rootReadme = await readText('README.md');
assert.match(rootReadme, /npx @kaelio\/ktx setup demo --no-input/);
assert.match(rootReadme, /npx @kaelio\/ktx sl query/);
assert.match(rootReadme, /npm install @kaelio\/ktx/);
assert.match(rootReadme, /npm install -g @kaelio\/ktx/);
assert.match(rootReadme, /ktx runtime install/);
assert.match(rootReadme, /ktx runtime status/);
assert.match(rootReadme, /ktx runtime doctor/);
assert.match(rootReadme, /ktx runtime start/);
assert.match(rootReadme, /ktx runtime stop/);
assert.match(rootReadme, /ktx serve --mcp stdio/);
assert.doesNotMatch(rootReadme, /uv run ktx-daemon serve-http/);
assert.doesNotMatch(rootReadme, /--semantic-compute-url http:\/\/127\.0\.0\.1:8765/);
});
```
- [ ] **Step 2: Add package artifact README assertions**
In `scripts/examples-docs.test.mjs`, insert this test after the new public
runtime README test:
```javascript
it('documents the public package artifact smoke shape', async () => {
const readme = await readText('examples/package-artifacts/README.md');
assert.match(readme, /@kaelio\/ktx/);
assert.match(readme, /managed Python runtime/);
assert.match(readme, /ktx runtime status/);
assert.match(readme, /ktx runtime doctor/);
assert.doesNotMatch(readme, /@ktx\/context/);
assert.doesNotMatch(readme, /@ktx\/cli/);
assert.doesNotMatch(readme, /python -m ktx_daemon semantic-validate/);
});
```
- [ ] **Step 3: Extend Postgres smoke assertions**
In the existing `documents the Postgres historic SQL smoke example` test in
`scripts/examples-docs.test.mjs`, add these assertions after
`assert.match(smoke, /pg_stat_statements_reset/);`:
```javascript
assert.match(smoke, /KTX_RUNTIME_ROOT/);
assert.match(smoke, /managedDaemon/);
assert.match(smoke, /installPolicy: 'auto'/);
assert.match(smoke, /getKtxCliPackageInfo/);
assert.doesNotMatch(smoke, /python-service/);
assert.doesNotMatch(smoke, /PYTHON_SERVICE/);
assert.doesNotMatch(smoke, /uvicorn app\.main:app/);
assert.doesNotMatch(smoke, /export KTX_SQL_ANALYSIS_URL/);
assert.doesNotMatch(readme, /python-service/);
assert.doesNotMatch(readme, /KTX_SQL_ANALYSIS_URL/);
```
- [ ] **Step 4: Run the docs test to verify it fails**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: FAIL. The failure includes missing `@kaelio/ktx` README matches and
the existing `python-service` / `KTX_SQL_ANALYSIS_URL` references in the
Postgres smoke files.
### Task 2: Move the Postgres historic smoke to the managed runtime
**Files:**
- Modify: `examples/postgres-historic/scripts/smoke.sh`
- Test: `scripts/examples-docs.test.mjs`
- [ ] **Step 1: Remove Python service process state**
In `examples/postgres-historic/scripts/smoke.sh`, replace the variable block:
```bash
KTX_BIN="$KTX_ROOT/packages/cli/dist/bin.js"
PYTHON_SERVICE_LOG="$PROJECT_PARENT/python-service.log"
PYTHON_SERVICE_PID=""
```
with:
```bash
KTX_BIN="$KTX_ROOT/packages/cli/dist/bin.js"
export KTX_RUNTIME_ROOT="$PROJECT_PARENT/managed-runtime"
unset KTX_DAEMON_URL
unset KTX_SQL_ANALYSIS_URL
```
- [ ] **Step 2: Replace cleanup**
In `examples/postgres-historic/scripts/smoke.sh`, replace the `cleanup()`
function with:
```bash
cleanup() {
if [[ -f "$KTX_BIN" ]]; then
node "$KTX_BIN" runtime stop >/dev/null 2>&1 || true
fi
if [[ "${KTX_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
```
- [ ] **Step 3: Delete the old SQL analysis service starter**
Delete the entire `start_sql_analysis_if_needed()` function from
`examples/postgres-historic/scripts/smoke.sh`. The deleted function begins with
this line:
```bash
start_sql_analysis_if_needed() {
```
and ends with this line:
```bash
}
```
immediately before the `latest_manifest()` function.
- [ ] **Step 4: Pass managed daemon options to stage-only ingest**
In the Node heredoc inside `run_historic_stage_only()`, replace this block:
```javascript
const { createKtxCliLocalIngestAdapters } = await import(join(ktxRoot, 'packages/cli/dist/local-adapters.js'));
const project = await loadKtxProject({ projectDir });
const adapters = createKtxCliLocalIngestAdapters(project, { historicSqlConnectionId: 'warehouse' });
```
with:
```javascript
const { createKtxCliLocalIngestAdapters } = await import(join(ktxRoot, 'packages/cli/dist/local-adapters.js'));
const { getKtxCliPackageInfo } = await import(join(ktxRoot, 'packages/cli/dist/index.js'));
const project = await loadKtxProject({ projectDir });
const cliVersion = getKtxCliPackageInfo().version;
const managedRuntimeIo = { stdout: process.stdout, stderr: process.stderr };
const adapters = createKtxCliLocalIngestAdapters(project, {
historicSqlConnectionId: 'warehouse',
managedDaemon: {
cliVersion,
installPolicy: 'auto',
io: managedRuntimeIo,
},
});
```
- [ ] **Step 5: Remove the old starter call**
Delete this line from the bottom half of
`examples/postgres-historic/scripts/smoke.sh`:
```bash
start_sql_analysis_if_needed
```
- [ ] **Step 6: Run the docs test to verify the script guards pass**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: FAIL remains because README files have not been updated yet. The
Postgres smoke script assertions now pass.
### Task 3: Update Postgres historic and artifact docs
**Files:**
- Modify: `examples/postgres-historic/README.md`
- Modify: `examples/package-artifacts/README.md`
- Test: `scripts/examples-docs.test.mjs`
- [ ] **Step 1: Replace Postgres prerequisites**
In `examples/postgres-historic/README.md`, replace the `## Prerequisites`
section with:
```markdown
## Prerequisites
- Docker with Compose v2
- Node and pnpm matching the KTX workspace
- `uv` on `PATH` so the KTX-managed Python runtime can install the bundled
runtime wheel
```
- [ ] **Step 2: Replace the smoke run description**
In `examples/postgres-historic/README.md`, replace the paragraph after the
`examples/postgres-historic/scripts/smoke.sh` command with:
```markdown
The smoke creates a temporary KTX project, isolates the managed Python runtime
under the temporary project parent, starts Postgres on `127.0.0.1:55432`, and
uses this connection URL:
```
- [ ] **Step 3: Update the full ingest command**
In `examples/postgres-historic/README.md`, replace the manual ingest command:
```bash
node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic dev ingest run \
--connection-id warehouse \
--adapter historic-sql \
--plain \
--no-input
```
with:
```bash
pnpm run ktx -- dev ingest run --project-dir /tmp/ktx-postgres-historic \
--connection-id warehouse \
--adapter historic-sql \
--plain \
--yes \
--no-input
```
- [ ] **Step 4: Replace SQL-analysis troubleshooting**
In `examples/postgres-historic/README.md`, replace the final troubleshooting
bullet:
```markdown
- SQL-analysis failures: set `KTX_SQL_ANALYSIS_URL` to the running service URL
or create `python-service/.venv` before running `scripts/smoke.sh`.
```
with:
```markdown
- SQL-analysis failures: run `pnpm run ktx -- runtime doctor` from the KTX
repository root and confirm `uv`, the bundled Python wheel, and the managed
runtime all pass.
```
- [ ] **Step 5: Replace package artifact README body**
Replace the full contents of `examples/package-artifacts/README.md` with:
````markdown
# Package artifact smoke checks
The package artifact smoke checks create temporary projects instead of storing
sample projects in this directory. Run the checks from `ktx/`:
```bash
pnpm run artifacts:check
```
The npm smoke project installs the generated public `@kaelio/ktx` tarball,
imports the package entry point, and runs installed `ktx` commands against a
generated local project.
The managed runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
daemon, and stops it.
The Python smoke project still installs the Python artifacts directly because
it verifies the standalone Python distributions that feed the bundled runtime
wheel.
````
- [ ] **Step 6: Run the docs test to verify these docs pass**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: FAIL remains because `README.md` still lacks the public npm managed
runtime documentation. The Postgres and package artifact assertions now pass.
### Task 4: Update the root README public runtime path
**Files:**
- Modify: `README.md`
- Test: `scripts/examples-docs.test.mjs`
- [ ] **Step 1: Replace quick start**
In `README.md`, replace the `## Quick start` section through the end of the
full-demo paragraph with:
````markdown
## Quick start
Run the pre-seeded demo through the public npm package:
```bash
npx @kaelio/ktx setup demo --no-input
npx @kaelio/ktx setup demo inspect
```
The default demo uses packaged sample data and prebuilt context. It does not
require API keys, network access, or an LLM provider.
To replay the packaged ingest run, use:
```bash
npx @kaelio/ktx setup demo --mode replay --no-input
```
To run the full agentic demo with an LLM provider, set a provider key for the
current process:
```bash
ANTHROPIC_API_KEY=$YOUR_ANTHROPIC_API_KEY \
npx @kaelio/ktx setup demo --mode full --no-input
```
Interactive full-demo setup can prompt for a provider key without writing the
key to `ktx.yaml`.
You can also install the CLI in a project or globally:
```bash
npm install @kaelio/ktx
npx ktx --help
npm install -g @kaelio/ktx
ktx --help
```
````
- [ ] **Step 2: Replace local project setup command**
In the `## Build a local project` section of `README.md`, replace:
```bash
uv sync --all-packages
source .venv/bin/activate
PROJECT_DIR="$(mktemp -d)/ktx-demo"
pnpm run ktx -- init "$PROJECT_DIR" --name ktx-demo
```
with:
```bash
npm install @kaelio/ktx
PROJECT_DIR="$(mktemp -d)/ktx-demo"
npx ktx init "$PROJECT_DIR" --name ktx-demo
```
- [ ] **Step 3: Replace README command prefixes**
In `README.md`, replace the source-tree command prefix `pnpm run ktx --` with
`npx ktx` in all user workflow commands under `## Build a local project`,
`### Scan the demo warehouse`, and `## Serve MCP`. Keep `pnpm run ktx --` in
the `## Development` section.
For example, this command:
```bash
pnpm run ktx -- sl query --project-dir "$PROJECT_DIR" \
```
becomes:
```bash
npx ktx sl query --project-dir "$PROJECT_DIR" \
```
- [ ] **Step 4: Add managed runtime section**
Insert this section after the scan walkthrough in `README.md`:
````markdown
## Managed Python runtime
KTX installs its Python runtime only when a Python-backed command needs it.
The runtime lives outside the npm cache, is versioned by the installed CLI
version, and is managed by `ktx runtime` commands:
```bash
npx ktx runtime install --yes
npx ktx runtime status
npx ktx runtime doctor
npx ktx runtime start
npx ktx runtime stop
```
Commands such as `npx @kaelio/ktx sl query ... --yes` can install the core
runtime lazily from the bundled wheel. Local embeddings remain lazy; prepare
them only when you select local `sentence-transformers` embeddings:
```bash
npx ktx runtime install --feature local-embeddings --yes
npx ktx runtime start --feature local-embeddings
```
````
- [ ] **Step 5: Replace Serve MCP section**
In `README.md`, replace the full `## Serve MCP` section with:
````markdown
## Serve MCP
Start the stdio MCP server from the project directory:
```bash
npx ktx serve --mcp stdio --project-dir "$PROJECT_DIR" \
--user-id local \
--semantic-compute \
--execute-queries \
--yes
```
The `--semantic-compute` flag uses the managed Python runtime when no explicit
semantic compute URL is provided. KTX starts or reuses the managed runtime as
needed.
The MCP server exposes `connection_list`, `knowledge_search`,
`knowledge_read`, `knowledge_write`, `sl_list_sources`, `sl_read_source`,
`sl_write_source`, `sl_validate`, `sl_query`, `ingest_trigger`,
`ingest_status`, `ingest_report`, and `ingest_replay`.
````
- [ ] **Step 6: Update release status wording**
In `README.md`, replace this sentence in `## Release status`:
```markdown
This repository is prepared for source publication. Package publishing is still
disabled by `release-policy.json`; registry names, public versions, package
visibility, and provenance policy must be chosen before publishing artifacts to
npm or Python package indexes.
```
with:
```markdown
This repository builds a single public npm artifact named `@kaelio/ktx`.
Package publishing is still disabled by `release-policy.json`; registry
credentials, public versions, release tags, and provenance policy must be
chosen before publishing artifacts to npm or Python package indexes.
```
- [ ] **Step 7: Run the docs test to verify the README passes**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: PASS.
### Task 5: Final verification and commit
**Files:**
- Verify: `scripts/examples-docs.test.mjs`
- Verify: `examples/postgres-historic/scripts/smoke.sh`
- Verify: `examples/postgres-historic/README.md`
- Verify: `examples/package-artifacts/README.md`
- Verify: `README.md`
- [ ] **Step 1: Run the script test suite affected by docs**
Run:
```bash
node --test scripts/examples-docs.test.mjs scripts/check-boundaries.test.mjs
```
Expected: PASS.
- [ ] **Step 2: Run the boundary check**
Run:
```bash
node scripts/check-boundaries.mjs
```
Expected:
```text
ktx boundary check passed
```
- [ ] **Step 3: Search for removed external runtime references**
Run:
```bash
rg -n "python-service|uvicorn app\\.main:app|export KTX_SQL_ANALYSIS_URL|uv run ktx-daemon serve-http|@ktx/context.*@ktx/cli" README.md examples/postgres-historic/README.md examples/postgres-historic/scripts/smoke.sh examples/package-artifacts/README.md
```
Expected: no matches.
- [ ] **Step 4: Commit**
```bash
git add scripts/examples-docs.test.mjs \
examples/postgres-historic/scripts/smoke.sh \
examples/postgres-historic/README.md \
examples/package-artifacts/README.md \
README.md
git commit -m "docs: align managed runtime examples"
```
## Acceptance criteria
- The Postgres historic SQL smoke no longer references `python-service/`,
`uvicorn app.main:app`, or `export KTX_SQL_ANALYSIS_URL`.
- The stage-only Postgres historic smoke uses `createKtxCliLocalIngestAdapters`
with managed daemon options and `installPolicy: 'auto'`.
- The root README documents `npx @kaelio/ktx`, local `npx ktx`, global `ktx`,
`ktx runtime ...`, and MCP `--semantic-compute --yes` managed-runtime usage.
- Package artifact docs describe the single public `@kaelio/ktx` tarball and
the managed runtime smoke.
- `node --test scripts/examples-docs.test.mjs scripts/check-boundaries.test.mjs`
passes.
- `node scripts/check-boundaries.mjs` passes.
## Self-review
- Spec coverage: This plan covers the remaining user-facing drift from the
npm-managed runtime spec by removing manual Python service guidance,
documenting public `@kaelio/ktx` invocation modes, and making the Postgres
example smoke use the managed core daemon.
- Placeholder scan: The plan contains exact files, edits, commands, expected
outcomes, and commit instructions.
- Type consistency: The plan uses the existing `managedDaemon` option shape
from `packages/cli/src/local-adapters.ts` and the existing
`installPolicy: 'auto'` value from `packages/cli/src/managed-python-command.ts`.

View file

@ -0,0 +1,377 @@
# Managed Runtime Prune Smoke and Docs 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:** Prove and document `ktx runtime prune` as part of the npm-managed
Python runtime release contract.
**Architecture:** The prune command already exists in the CLI runtime layer, so
this plan adds black-box package smoke coverage and public documentation only.
The smoke creates an isolated stale versioned runtime directory, previews it,
verifies confirmation is required, and removes it through the installed
`@kaelio/ktx` package.
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, Markdown, KTX CLI
managed Python runtime.
---
## Current state
This plan follows
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
The following plan files are based on that spec and are implemented in the
current tree:
- `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`
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
Implementation evidence found before writing this plan includes:
- `packages/cli/assets/python/manifest.json` and
`packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl`.
- `packages/cli/src/managed-python-runtime.ts`, including
`installManagedPythonRuntime()`, `doctorManagedPythonRuntime()`, and
`pruneManagedPythonRuntimes()`.
- `packages/cli/src/runtime.ts`, including the `install`, `status`,
`doctor`, `start`, `stop`, and `prune` runtime command runner branches.
- `packages/cli/src/commands/runtime-commands.ts`, including the
`runtime prune --dry-run` and `runtime prune --yes` Commander wiring.
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
`scripts/published-package-smoke.mjs`, `scripts/local-embeddings-runtime-smoke.mjs`,
`scripts/publish-public-npm-package.mjs`, `release-policy.json`, and
`.github/workflows/release.yml`.
- `README.md` and `examples/package-artifacts/README.md` document the managed
runtime but do not mention `ktx runtime prune`.
The remaining gap is narrow: the spec lists `ktx runtime prune` as part of the
runtime management command family, but public docs and installed package smoke
coverage only prove `install`, `status`, `doctor`, `start`, and `stop`.
## File structure
- Modify `scripts/package-artifacts.test.mjs`: assert that the generated
installed npm smoke covers `ktx runtime prune --dry-run`, confirmation
failure, and confirmed deletion.
- Modify `scripts/package-artifacts.mjs`: extend `npmRuntimeSmokeSource()` to
create a stale runtime directory and exercise `ktx runtime prune`.
- Modify `scripts/examples-docs.test.mjs`: require public docs to mention
`ktx runtime prune --dry-run` and `ktx runtime prune --yes`.
- Modify `README.md`: add prune commands and one sentence describing preview
and confirmed deletion.
- Modify `examples/package-artifacts/README.md`: describe prune coverage in the
package artifact smoke.
### Task 1: Add installed package prune smoke coverage
**Files:**
- Modify: `scripts/package-artifacts.test.mjs`
- Modify: `scripts/package-artifacts.mjs`
- [ ] **Step 1: Add failing smoke-source assertions**
In `scripts/package-artifacts.test.mjs`, inside
`it('runs installed CLI commands through the public package runtime', () => {`
and immediately after the existing assertions for `ktx runtime stop`, add:
```javascript
assert.match(source, /ktx runtime prune dry run/);
assert.match(source, /0\.0\.0/);
assert.match(source, /ktx runtime prune needs confirmation/);
assert.match(source, /Refusing to prune without --yes/);
assert.match(source, /ktx runtime prune confirmed/);
assert.match(source, /Removed stale KTX Python runtimes/);
assert.match(source, /assert\.rejects\(\(\) => access\(staleRuntimeDir\)\)/);
```
- [ ] **Step 2: Run the package artifact test and verify failure**
Run:
```bash
node --test scripts/package-artifacts.test.mjs
```
Expected: FAIL in the installed CLI smoke source test because
`npmRuntimeSmokeSource()` does not yet contain the prune labels, confirmation
guard, or stale runtime removal assertion.
- [ ] **Step 3: Extend the generated installed CLI smoke**
In `scripts/package-artifacts.mjs`, inside `npmRuntimeSmokeSource()`, add this
block immediately after:
```javascript
process.stdout.write('ktx runtime daemon lifecycle verified\n');
```
Add:
```javascript
const staleRuntimeDir = join(process.env.KTX_RUNTIME_ROOT, '0.0.0');
await mkdir(staleRuntimeDir, { recursive: true });
const runtimePruneDryRun = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune', '--dry-run']);
requireSuccess('ktx runtime prune dry run', runtimePruneDryRun);
requireOutput('ktx runtime prune dry run', runtimePruneDryRun, /Stale KTX Python runtimes/);
requireOutput('ktx runtime prune dry run', runtimePruneDryRun, /0\.0\.0/);
await access(staleRuntimeDir);
const runtimePruneNeedsConfirmation = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune']);
assert.equal(runtimePruneNeedsConfirmation.code, 1, 'ktx runtime prune without --yes must fail');
assert.equal(runtimePruneNeedsConfirmation.stdout, '', 'ktx runtime prune confirmation failure wrote stdout');
assert.match(runtimePruneNeedsConfirmation.stderr, /Refusing to prune without --yes/);
const runtimePruneConfirmed = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune', '--yes']);
requireSuccess('ktx runtime prune confirmed', runtimePruneConfirmed);
requireOutput('ktx runtime prune confirmed', runtimePruneConfirmed, /Removed stale KTX Python runtimes/);
requireOutput('ktx runtime prune confirmed', runtimePruneConfirmed, /0\.0\.0/);
await assert.rejects(() => access(staleRuntimeDir));
process.stdout.write('ktx runtime prune verified\n');
```
No import changes are needed because the generated smoke already imports
`assert`, `access`, `mkdir`, and `join`.
- [ ] **Step 4: Run the package artifact test and verify pass**
Run:
```bash
node --test scripts/package-artifacts.test.mjs
```
Expected: PASS. The source assertions now find prune dry-run coverage,
confirmation failure coverage, confirmed prune coverage, and stale directory
deletion verification.
- [ ] **Step 5: Commit the smoke coverage**
Run:
```bash
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
git commit -m "test: cover managed runtime prune in package smoke"
```
### Task 2: Document runtime prune in public docs
**Files:**
- Modify: `scripts/examples-docs.test.mjs`
- Modify: `README.md`
- Modify: `examples/package-artifacts/README.md`
- [ ] **Step 1: Add failing docs assertions**
In `scripts/examples-docs.test.mjs`, inside
`it('documents public npm and managed runtime usage in the README', async () => {`
and immediately after:
```javascript
assert.match(rootReadme, /ktx runtime stop/);
```
Add:
```javascript
assert.match(rootReadme, /ktx runtime prune --dry-run/);
assert.match(rootReadme, /ktx runtime prune --yes/);
```
In the same file, inside
`it('documents the public package artifact smoke shape', async () => {` and
immediately after:
```javascript
assert.match(readme, /ktx runtime doctor/);
```
Add:
```javascript
assert.match(readme, /ktx runtime prune --dry-run/);
assert.match(readme, /ktx runtime prune --yes/);
```
- [ ] **Step 2: Run the docs test and verify failure**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: FAIL because `README.md` and
`examples/package-artifacts/README.md` do not yet mention `ktx runtime prune`.
- [ ] **Step 3: Update the root README runtime section**
In `README.md`, in the `## Managed Python runtime` command block, replace:
```bash
npx ktx runtime install --yes
npx ktx runtime status
npx ktx runtime doctor
npx ktx runtime start
npx ktx runtime stop
```
with:
```bash
npx ktx runtime install --yes
npx ktx runtime status
npx ktx runtime doctor
npx ktx runtime start
npx ktx runtime stop
npx ktx runtime prune --dry-run
npx ktx runtime prune --yes
```
Immediately after that command block, add:
```markdown
Use `runtime prune --dry-run` to preview stale runtime directories from older
CLI versions. Add `--yes` to remove those stale directories after daemon
processes are stopped.
```
- [ ] **Step 4: Update package artifact smoke docs**
In `examples/package-artifacts/README.md`, replace:
```markdown
The managed Python runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
daemon, and stops it.
```
with:
```markdown
The managed Python runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
daemon, stops it, previews a stale runtime with `ktx runtime prune --dry-run`,
verifies confirmation is required, and removes the stale runtime with
`ktx runtime prune --yes`.
```
- [ ] **Step 5: Run the docs test and verify pass**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: PASS. The public README and package artifact README now document
runtime prune alongside the other managed runtime commands.
- [ ] **Step 6: Commit the docs coverage**
Run:
```bash
git add scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
git commit -m "docs: document managed runtime prune"
```
### Task 3: Verify the completed prune release surface
**Files:**
- Verify: `scripts/package-artifacts.mjs`
- Verify: `scripts/package-artifacts.test.mjs`
- Verify: `scripts/examples-docs.test.mjs`
- Verify: `README.md`
- Verify: `examples/package-artifacts/README.md`
- [ ] **Step 1: Run focused tests**
Run:
```bash
node --test scripts/package-artifacts.test.mjs scripts/examples-docs.test.mjs
```
Expected: PASS. The source-level tests cover generated package smoke behavior
and docs assertions.
- [ ] **Step 2: Run the installed package artifact smoke**
Run:
```bash
pnpm run artifacts:check
```
Expected: PASS. The generated installed CLI smoke prints:
```text
ktx runtime prune verified
```
and removes the temporary `0.0.0` directory from the isolated
`KTX_RUNTIME_ROOT`.
- [ ] **Step 3: Inspect git status**
Run:
```bash
git status --short
```
Expected: only the five planned files are modified before the final commit, or
no modified files remain after the task commits.
- [ ] **Step 4: Commit verification fixes if needed**
If verification required small corrections, commit only those intended files:
```bash
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
git commit -m "test: verify managed runtime prune release surface"
```
## Acceptance criteria
- The generated installed npm package smoke creates a stale versioned runtime
directory under the isolated `KTX_RUNTIME_ROOT`.
- `ktx runtime prune --dry-run` lists the stale runtime and leaves it on disk.
- `ktx runtime prune` without `--yes` exits nonzero and prints the existing
confirmation guidance.
- `ktx runtime prune --yes` removes the stale runtime directory.
- `README.md` lists `ktx runtime prune --dry-run` and
`ktx runtime prune --yes` with the other managed runtime commands.
- `examples/package-artifacts/README.md` describes prune coverage in the
package artifact smoke.
## Self-review
- Spec coverage: this plan covers the remaining visible gap for the runtime
management command family in the npm-managed Python runtime spec. The prune
implementation already exists, and this plan adds release smoke and public
docs coverage.
- Placeholder scan: no placeholder steps, deferred implementation notes, or
unspecified behavior gaps remain.
- Type consistency: the plan uses existing labels and functions:
`npmRuntimeSmokeSource()`, `requireSuccess()`, `requireOutput()`,
`KTX_RUNTIME_ROOT`, `ktx runtime prune --dry-run`, and
`ktx runtime prune --yes`.

View file

@ -0,0 +1,647 @@
# Managed Runtime uv Prerequisite Contract 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:** Close the remaining npm-managed Python runtime open decision by
making `uv` a documented, release-policy-checked prerequisite.
**Architecture:** Keep the runtime installer behavior simple: the CLI locates
`uv` on `PATH` and prints a focused error when it is missing. Encode that
decision in `release-policy.json`, validate it during release readiness, use one
shared runtime error message, and document the prerequisite in public docs.
**Tech Stack:** Node 22 ESM scripts, `node:test`, TypeScript, Vitest, JSON
release policy, Markdown.
---
## Existing status
This plan is based on
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
The following plan files 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`
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-prune-smoke-and-docs.md`
Implementation evidence found before writing this plan includes:
- `packages/cli/assets/python/manifest.json` and the bundled
`kaelio_ktx-0.1.0-py3-none-any.whl`.
- `packages/cli/src/managed-python-runtime.ts`, including runtime roots,
bundled wheel verification, install, status, doctor, and prune behavior.
- `packages/cli/src/managed-python-command.ts`,
`packages/cli/src/managed-python-daemon.ts`,
`packages/cli/src/managed-local-embeddings.ts`, and
`packages/cli/src/managed-python-http.ts`.
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
`scripts/published-package-smoke.mjs`,
`scripts/local-embeddings-runtime-smoke.mjs`, and
`scripts/publish-public-npm-package.mjs`.
- `release-policy.json` is already in `npm-public-release-ready` mode for
`@kaelio/ktx` `0.1.0` and keeps Python package publishing disabled.
- `README.md` and `examples/package-artifacts/README.md` document the managed
runtime command family, including `runtime prune`.
The remaining spec gap is the open decision in
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`:
```text
KTX still needs a final decision on whether uv is a hard prerequisite or a
bootstrap dependency that KTX downloads automatically.
```
This plan chooses the hard-prerequisite path for the first public release. KTX
will not download `uv` automatically in this release.
## File structure
- Modify `release-policy.json`: add a `runtimeInstaller` policy section that
records the hard `uv` prerequisite decision.
- Modify `scripts/release-readiness.mjs`: validate the runtime installer
policy, include it in readiness reports, and print it in text output.
- Modify `scripts/release-readiness.test.mjs`: cover the accepted policy and
rejection paths for missing or bootstrap-style `uv` policies.
- Modify `packages/cli/src/managed-python-runtime.ts`: export one shared
missing-`uv` message and use it for install and doctor output.
- Modify `packages/cli/src/managed-python-runtime.test.ts`: cover install and
doctor behavior when `uv` is missing.
- Modify `scripts/examples-docs.test.mjs`: require public docs to state the
hard `uv` prerequisite.
- Modify `README.md`: document that `uv` must be on `PATH` and KTX does not
download it automatically.
- Modify `examples/package-artifacts/README.md`: document the artifact smoke
`uv` prerequisite.
### Task 1: Encode the runtime installer policy
**Files:**
- Modify: `release-policy.json`
- Modify: `scripts/release-readiness.test.mjs`
- Modify: `scripts/release-readiness.mjs`
- Test: `scripts/release-readiness.test.mjs`
- [ ] **Step 1: Add failing release policy tests**
In `scripts/release-readiness.test.mjs`, inside the `releasePolicy()` helper
return value, add the `runtimeInstaller` object immediately after
`publishedPackageSmoke`:
```javascript
runtimeInstaller: {
uvStrategy: 'path-prerequisite',
bootstrapUv: false,
missingUvBehavior: 'focused-error',
},
```
In the three `assert.deepEqual(report, { ... })` expectations, add this field
immediately after `publishedPackageSmokeGate`:
```javascript
runtimeInstaller: {
uvStrategy: 'path-prerequisite',
bootstrapUv: false,
missingUvBehavior: 'focused-error',
},
```
Add these tests immediately after the
`it('accepts the npm public release ready policy', async () => { ... })` block:
```javascript
it('rejects npm public release ready mode without a runtime installer policy', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-runtime-policy-missing-test-'));
try {
await writeReadyFixture(root, {
policy: releasePolicy({
releaseMode: 'npm-public-release-ready',
npm: {
publish: true,
registry: null,
access: 'public',
tag: 'latest',
},
publishedPackageSmoke: {
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
registry: null,
},
runtimeInstaller: undefined,
requiredBeforePublishing: [],
}),
});
await assert.rejects(
() => releaseReadinessReport(root),
/Release policy runtimeInstaller must be a JSON object/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it('rejects uv bootstrap download policy for the first public npm release', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-runtime-policy-bootstrap-test-'));
try {
await writeReadyFixture(root, {
policy: releasePolicy({
releaseMode: 'npm-public-release-ready',
npm: {
publish: true,
registry: null,
access: 'public',
tag: 'latest',
},
publishedPackageSmoke: {
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
registry: null,
},
runtimeInstaller: {
uvStrategy: 'bootstrap-download',
bootstrapUv: true,
missingUvBehavior: 'download',
},
requiredBeforePublishing: [],
}),
});
await assert.rejects(
() => releaseReadinessReport(root),
/Release policy runtimeInstaller\.uvStrategy must be path-prerequisite/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
```
- [ ] **Step 2: Run the release readiness tests and verify failure**
Run:
```bash
node --test scripts/release-readiness.test.mjs
```
Expected: FAIL because `releaseReadinessReport()` does not include
`runtimeInstaller`, and `validateReleasePolicy()` does not validate the new
policy section.
- [ ] **Step 3: Validate the runtime installer policy**
In `scripts/release-readiness.mjs`, add this function immediately after the
`assertRequiredBeforePublishing(policy)` function definition:
```javascript
function assertRuntimeInstallerPolicy(policy) {
assertPlainObject(policy.runtimeInstaller, 'Release policy runtimeInstaller');
assertString(policy.runtimeInstaller.uvStrategy, 'Release policy runtimeInstaller.uvStrategy');
assertBoolean(policy.runtimeInstaller.bootstrapUv, 'Release policy runtimeInstaller.bootstrapUv');
assertString(
policy.runtimeInstaller.missingUvBehavior,
'Release policy runtimeInstaller.missingUvBehavior',
);
if (policy.runtimeInstaller.uvStrategy !== 'path-prerequisite') {
throw new Error('Release policy runtimeInstaller.uvStrategy must be path-prerequisite');
}
if (policy.runtimeInstaller.bootstrapUv !== false) {
throw new Error('Release policy runtimeInstaller.bootstrapUv must be false');
}
if (policy.runtimeInstaller.missingUvBehavior !== 'focused-error') {
throw new Error('Release policy runtimeInstaller.missingUvBehavior must be focused-error');
}
}
```
In `validateReleasePolicy(policy)`, add this call immediately after
`assertRequiredBeforePublishing(policy);`:
```javascript
assertRuntimeInstallerPolicy(policy);
```
In `releaseReadinessReport(rootDir = scriptRootDir())`, add
`runtimeInstaller` to the returned object immediately after
`publishedPackageSmokeGate`:
```javascript
runtimeInstaller: policy.runtimeInstaller,
```
In `main()`, add these lines immediately after the published package smoke
registry line:
```javascript
process.stdout.write(`Runtime uv strategy: ${report.runtimeInstaller.uvStrategy}\n`);
process.stdout.write(
`Runtime uv bootstrap: ${report.runtimeInstaller.bootstrapUv ? 'enabled' : 'disabled'}\n`,
);
```
- [ ] **Step 4: Encode the policy in `release-policy.json`**
Replace `release-policy.json` with this exact content:
```json
{
"schemaVersion": 1,
"releaseMode": "npm-public-release-ready",
"npm": {
"publish": true,
"registry": null,
"access": "public",
"tag": "latest",
"packages": ["@kaelio/ktx"]
},
"python": {
"publish": false,
"repository": null,
"packages": ["ktx-sl", "ktx-daemon", "kaelio-ktx"]
},
"publishedPackageSmoke": {
"packageName": "@kaelio/ktx",
"version": "0.1.0",
"registry": null
},
"runtimeInstaller": {
"uvStrategy": "path-prerequisite",
"bootstrapUv": false,
"missingUvBehavior": "focused-error"
},
"requiredBeforePublishing": []
}
```
- [ ] **Step 5: Run the release readiness tests and verify success**
Run:
```bash
node --test scripts/release-readiness.test.mjs
```
Expected: PASS.
- [ ] **Step 6: Commit the release policy contract**
```bash
git add release-policy.json scripts/release-readiness.mjs scripts/release-readiness.test.mjs
git commit -m "chore: encode uv runtime prerequisite policy"
```
### Task 2: Centralize missing-uv runtime output
**Files:**
- Modify: `packages/cli/src/managed-python-runtime.test.ts`
- Modify: `packages/cli/src/managed-python-runtime.ts`
- Test: `packages/cli/src/managed-python-runtime.test.ts`
- [ ] **Step 1: Add failing missing-uv runtime tests**
In `packages/cli/src/managed-python-runtime.test.ts`, add
`MISSING_UV_RUNTIME_INSTALL_MESSAGE` to the import from
`./managed-python-runtime.js`:
```typescript
import {
MISSING_UV_RUNTIME_INSTALL_MESSAGE,
doctorManagedPythonRuntime,
installManagedPythonRuntime,
managedPythonRuntimeLayout,
pruneManagedPythonRuntimes,
readManagedPythonRuntimeStatus,
verifyRuntimeAsset,
type ManagedPythonRuntimeExec,
} from './managed-python-runtime.js';
```
Inside `describe('installManagedPythonRuntime', () => { ... })`, add this test
after the local embeddings test:
```typescript
it('fails with the hard-prerequisite message when uv is missing', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const commands: Array<{ command: string; args: string[] }> = [];
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
commands.push({ command, args });
throw new Error('spawn uv ENOENT');
});
await expect(
installManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
features: ['core'],
exec,
}),
).rejects.toThrow(MISSING_UV_RUNTIME_INSTALL_MESSAGE);
expect(commands).toEqual([{ command: 'uv', args: ['--version'] }]);
});
```
Inside `describe('doctorManagedPythonRuntime', () => { ... })`, add this test
after the existing doctor test:
```typescript
it('reports uv as a hard prerequisite when uv is missing', async () => {
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
const exec: ManagedPythonRuntimeExec = vi.fn(async () => {
throw new Error('spawn uv ENOENT');
});
const checks = await doctorManagedPythonRuntime({
cliVersion: '0.2.0',
runtimeRoot: join(tempDir, 'runtime'),
assetDir,
exec,
});
expect(checks[0]).toEqual({
id: 'uv',
label: 'uv',
status: 'fail',
detail: MISSING_UV_RUNTIME_INSTALL_MESSAGE,
fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes',
});
});
```
- [ ] **Step 2: Run the runtime tests and verify failure**
Run:
```bash
pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts
```
Expected: FAIL because the shared message constant does not exist and the
doctor fix text still uses the older message.
- [ ] **Step 3: Add the shared missing-uv message**
In `packages/cli/src/managed-python-runtime.ts`, add this export immediately
after the `ManagedPythonRuntimePruneResult` interface:
```typescript
export const MISSING_UV_RUNTIME_INSTALL_MESSAGE =
'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx runtime install --yes';
```
Replace the body of the `catch` block in `ensureUv()` with:
```typescript
throw new Error(MISSING_UV_RUNTIME_INSTALL_MESSAGE);
```
In `doctorManagedPythonRuntime()`, replace the `fix` value for the `uv` check
with:
```typescript
fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes',
```
- [ ] **Step 4: Run the runtime tests and verify success**
Run:
```bash
pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts
```
Expected: PASS.
- [ ] **Step 5: Commit the runtime output contract**
```bash
git add packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts
git commit -m "fix: clarify missing uv runtime error"
```
### Task 3: Document the hard uv prerequisite
**Files:**
- Modify: `scripts/examples-docs.test.mjs`
- Modify: `README.md`
- Modify: `examples/package-artifacts/README.md`
- Test: `scripts/examples-docs.test.mjs`
- [ ] **Step 1: Add failing docs assertions**
In `scripts/examples-docs.test.mjs`, inside
`it('documents public npm and managed runtime usage in the README', ... )`, add
these assertions immediately after the existing `ktx runtime prune --yes`
assertion:
```javascript
assert.match(rootReadme, /KTX requires `uv` on `PATH`/);
assert.match(rootReadme, /KTX doesn't download `uv` automatically/);
```
Inside `it('documents the public package artifact smoke shape', ... )`, add
this assertion immediately after the `managed Python runtime` assertion:
```javascript
assert.match(readme, /requires `uv` on `PATH`/);
```
- [ ] **Step 2: Run the docs test and verify failure**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: FAIL because the README files do not state the hard `uv`
prerequisite.
- [ ] **Step 3: Update the root README runtime section**
In `README.md`, in the `## Managed Python runtime` section, replace this
paragraph:
```markdown
KTX installs its Python runtime only when a Python-backed command needs it.
The runtime lives outside the npm cache, is versioned by the installed CLI
version, and is managed by `ktx runtime` commands:
```
With:
```markdown
KTX installs its Python runtime only when a Python-backed command needs it.
The runtime lives outside the npm cache, is versioned by the installed CLI
version, and is managed by `ktx runtime` commands.
KTX requires `uv` on `PATH` to create the managed runtime. Install `uv` with
your system package manager or the official installer before running Python-
backed KTX commands. KTX doesn't download `uv` automatically; run
`ktx runtime doctor` if runtime installation fails:
```
- [ ] **Step 4: Update the package artifact smoke README**
In `examples/package-artifacts/README.md`, replace this paragraph:
```markdown
The managed Python runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
daemon, stops it, previews a stale runtime with `ktx runtime prune --dry-run`,
verifies confirmation is required, and removes the stale runtime with
`ktx runtime prune --yes`.
```
With:
```markdown
The managed Python runtime smoke requires `uv` on `PATH`, isolates
`KTX_RUNTIME_ROOT`, verifies `ktx runtime status`, runs `ktx sl query --yes` to
install the core runtime from the bundled wheel, checks `ktx runtime doctor`,
starts and reuses the managed daemon, stops it, previews a stale runtime with
`ktx runtime prune --dry-run`, verifies confirmation is required, and removes
the stale runtime with `ktx runtime prune --yes`.
```
- [ ] **Step 5: Run the docs test and verify success**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: PASS.
- [ ] **Step 6: Commit the public docs update**
```bash
git add README.md examples/package-artifacts/README.md scripts/examples-docs.test.mjs
git commit -m "docs: document uv runtime prerequisite"
```
### Task 4: Verify the completed contract
**Files:**
- Verify: `release-policy.json`
- Verify: `scripts/release-readiness.mjs`
- Verify: `scripts/release-readiness.test.mjs`
- Verify: `packages/cli/src/managed-python-runtime.ts`
- Verify: `packages/cli/src/managed-python-runtime.test.ts`
- Verify: `scripts/examples-docs.test.mjs`
- Verify: `README.md`
- Verify: `examples/package-artifacts/README.md`
- [ ] **Step 1: Run focused verification**
Run:
```bash
node --test scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs
pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts
```
Expected: PASS.
- [ ] **Step 2: Verify release readiness text output**
Run:
```bash
pnpm run release:readiness
```
Expected output includes:
```text
KTX release mode: npm-public-release-ready
Runtime uv strategy: path-prerequisite
Runtime uv bootstrap: disabled
NPM publish target: @kaelio/ktx@0.1.0 (latest)
```
- [ ] **Step 3: Verify no pre-commit config is required**
Run:
```bash
rg --files -g '.pre-commit-config.yaml' -g 'pre-commit-config.yaml'
```
Expected: no output and exit code 1. No Python files changed, so the repository
Python pre-commit requirement does not apply.
- [ ] **Step 4: Review the final diff**
Run:
```bash
git diff --stat
git diff -- release-policy.json scripts/release-readiness.mjs scripts/release-readiness.test.mjs packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
```
Expected: only the runtime installer policy, missing-`uv` message/tests, and
public docs changed.
- [ ] **Step 5: Commit final verification notes if needed**
If Task 4 produces only verification output and no file changes, skip this
step. If a correction was made during verification, commit it:
```bash
git add release-policy.json scripts/release-readiness.mjs scripts/release-readiness.test.mjs packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
git commit -m "chore: finish uv prerequisite release contract"
```
## Self-review
Spec coverage:
- The earlier implemented plans cover the single public npm package, bundled
Python wheel, managed runtime installer, runtime commands, daemon lifecycle,
local embeddings, Python-backed command integration, release smoke, published
smoke, docs cleanup, release handoff, and prune coverage.
- This plan closes the spec's remaining `uv` open decision by choosing
`path-prerequisite`, recording that decision in release policy, validating it
in release readiness, using one CLI error message, and documenting it.
- The plan keeps Python package publication disabled and keeps KTX-owned Python
code bundled in the npm package.
Placeholder scan:
- No task contains deferred implementation markers.
- Each code-changing step names exact files and includes the concrete code to
add or replace.
Type consistency:
- The release policy field is consistently named `runtimeInstaller`.
- The chosen strategy is consistently `path-prerequisite`.
- The shared CLI message constant is consistently
`MISSING_UV_RUNTIME_INSTALL_MESSAGE`.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,602 @@
# Published Package Managed Runtime Smoke 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 the post-publication smoke prove that the published
`@kaelio/ktx` package uses the same isolated managed Python runtime across
`npx @kaelio/ktx`, local `npx ktx`, and global `ktx` invocation modes.
**Architecture:** Keep the smoke black-box and network-gated. Strengthen the
command builder so every Python-backed published-package command receives the
same temporary `KTX_RUNTIME_ROOT`, then run a real semantic-layer query through
the direct `npx`, local install, and global install paths instead of checking
only `--version` for local and global binaries.
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, npx, KTX managed Python
runtime, published `@kaelio/ktx` package smoke.
---
## 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 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`
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.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`,
`packages/cli/src/commands/runtime-commands.ts`,
`packages/cli/src/managed-python-command.ts`,
`packages/cli/src/managed-python-daemon.ts`,
`packages/cli/src/managed-local-embeddings.ts`, and
`packages/cli/src/managed-python-http.ts`.
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
`scripts/local-embeddings-runtime-smoke.mjs`, and
`scripts/published-package-smoke.mjs`.
- `packages/cli/src/agent-runtime.ts`, `packages/cli/src/serve.ts`,
`packages/cli/src/ingest.ts`, and `packages/cli/src/scan.ts` thread managed
runtime policy through the Python-backed CLI paths.
- `examples/postgres-historic/scripts/smoke.sh`,
`examples/postgres-historic/README.md`,
`examples/package-artifacts/README.md`, and `README.md` now document the
managed runtime instead of a manual `python-service/` process.
The remaining release-confidence gap is in the post-publication smoke:
- `scripts/published-package-smoke-config.mjs` runs `npx @kaelio/ktx setup
demo` and `npx @kaelio/ktx sl query ... --yes`, but it does not isolate
`KTX_RUNTIME_ROOT` for those commands.
- The same smoke installs `@kaelio/ktx` locally and globally, but local and
global verification only run `--version`.
- The design spec requires the direct `npx @kaelio/ktx`, local `npx ktx`, and
global `ktx` modes to work for real KTX commands. A semantic-layer query is
the smallest Python-backed command that proves the bundled managed runtime is
usable in each mode.
## File structure
- Modify `scripts/published-package-smoke.test.mjs`: expect a shared
`KTX_RUNTIME_ROOT` in the published smoke commands, expect local and global
semantic query commands, and cover label classification used by the runner.
- Modify `scripts/published-package-smoke-config.mjs`: derive a temporary
runtime root from the smoke project directory, merge it with registry
environment settings, and add local and global `sl query` commands.
- Modify `scripts/published-package-smoke.mjs`: validate the renamed version
labels and semantic query labels when the smoke runs.
### Task 1: Isolate runtime roots and add real local/global command coverage
**Files:**
- Modify: `scripts/published-package-smoke.test.mjs`
- Modify: `scripts/published-package-smoke-config.mjs`
- Test: `scripts/published-package-smoke.test.mjs`
- [ ] **Step 1: Write the failing command-list test**
In `scripts/published-package-smoke.test.mjs`, replace the existing
`it('builds the full public package smoke command list', ...)` block with this
test:
```javascript
it('builds the full public package smoke command list', () => {
assert.deepEqual(
buildPublishedPackageSmokeCommands(
config,
'/tmp/ktx-smoke/demo',
'/tmp/ktx-smoke/managed-runtime',
),
[
{
label: 'published package npx version',
command: 'npx',
args: ['--yes', '@kaelio/ktx@latest', '--version'],
env: { npm_config_registry: 'https://registry.npmjs.org/' },
},
{
label: 'published package setup demo',
command: 'npx',
args: [
'--yes',
'@kaelio/ktx@latest',
'setup',
'demo',
'--project-dir',
'/tmp/ktx-smoke/demo',
'--no-input',
'--plain',
],
env: {
npm_config_registry: 'https://registry.npmjs.org/',
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
},
},
{
label: 'published package npx sl query',
command: 'npx',
args: [
'--yes',
'@kaelio/ktx@latest',
'sl',
'query',
'--project-dir',
'/tmp/ktx-smoke/demo',
'--connection-id',
'orbit_demo',
'--measure',
'contracts.contract_count',
'--format',
'sql',
'--yes',
],
env: {
npm_config_registry: 'https://registry.npmjs.org/',
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
},
},
{
label: 'published package local install',
command: 'pnpm',
args: ['add', '@kaelio/ktx@latest'],
env: { npm_config_registry: 'https://registry.npmjs.org/' },
},
{
label: 'published package local version',
command: 'npx',
args: ['ktx', '--version'],
env: { npm_config_registry: 'https://registry.npmjs.org/' },
},
{
label: 'published package local sl query',
command: 'npx',
args: [
'ktx',
'sl',
'query',
'--project-dir',
'/tmp/ktx-smoke/demo',
'--connection-id',
'orbit_demo',
'--measure',
'contracts.contract_count',
'--format',
'sql',
'--yes',
],
env: {
npm_config_registry: 'https://registry.npmjs.org/',
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
},
},
{
label: 'published package global install',
command: 'pnpm',
args: ['add', '--global', '@kaelio/ktx@latest'],
env: { npm_config_registry: 'https://registry.npmjs.org/' },
},
{
label: 'published package global version',
command: 'ktx',
args: ['--version'],
env: { npm_config_registry: 'https://registry.npmjs.org/' },
},
{
label: 'published package global sl query',
command: 'ktx',
args: [
'sl',
'query',
'--project-dir',
'/tmp/ktx-smoke/demo',
'--connection-id',
'orbit_demo',
'--measure',
'contracts.contract_count',
'--format',
'sql',
'--yes',
],
env: {
npm_config_registry: 'https://registry.npmjs.org/',
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
},
},
],
);
});
```
- [ ] **Step 2: Run the test and verify it fails**
Run:
```bash
node --test scripts/published-package-smoke.test.mjs
```
Expected: FAIL with an `AssertionError` showing that the actual command list
still uses `published package version`, lacks `KTX_RUNTIME_ROOT`, and lacks the
local/global `sl query` commands.
- [ ] **Step 3: Implement the command builder changes**
In `scripts/published-package-smoke-config.mjs`, add this import before the
existing `node:assert/strict` import:
```javascript
import { dirname, join } from 'node:path';
```
In the same file, add these helper functions after
`assertHttpRegistry(registry, label)`:
```javascript
function registryEnv(config) {
return config.registry ? { npm_config_registry: config.registry } : {};
}
function runtimeCommandEnv(config, runtimeRoot) {
return { ...registryEnv(config), KTX_RUNTIME_ROOT: runtimeRoot };
}
function semanticQueryArgs(projectDir) {
return [
'sl',
'query',
'--project-dir',
projectDir,
'--connection-id',
'orbit_demo',
'--measure',
'contracts.contract_count',
'--format',
'sql',
'--yes',
];
}
```
Replace `buildPublishedPackageNpxCommand()` and
`buildPublishedPackageSmokeCommands()` with this implementation:
```javascript
export function buildPublishedPackageNpxCommand(config, args, label = 'published package command', extraEnv = {}) {
return {
label,
command: 'npx',
args: ['--yes', publishedPackageSpec(config), ...args],
env: { ...registryEnv(config), ...extraEnv },
};
}
export function buildPublishedPackageSmokeCommands(
config,
projectDir,
runtimeRoot = join(dirname(projectDir), 'managed-runtime'),
) {
const runtimeEnv = runtimeCommandEnv(config, runtimeRoot);
const packageEnv = registryEnv(config);
const queryArgs = semanticQueryArgs(projectDir);
return [
buildPublishedPackageNpxCommand(config, ['--version'], 'published package npx version'),
buildPublishedPackageNpxCommand(
config,
['setup', 'demo', '--project-dir', projectDir, '--no-input', '--plain'],
'published package setup demo',
{ KTX_RUNTIME_ROOT: runtimeRoot },
),
buildPublishedPackageNpxCommand(config, queryArgs, 'published package npx sl query', {
KTX_RUNTIME_ROOT: runtimeRoot,
}),
{
label: 'published package local install',
command: 'pnpm',
args: ['add', publishedPackageSpec(config)],
env: packageEnv,
},
{
label: 'published package local version',
command: 'npx',
args: ['ktx', '--version'],
env: packageEnv,
},
{
label: 'published package local sl query',
command: 'npx',
args: ['ktx', ...queryArgs],
env: runtimeEnv,
},
{
label: 'published package global install',
command: 'pnpm',
args: ['add', '--global', publishedPackageSpec(config)],
env: packageEnv,
},
{
label: 'published package global version',
command: 'ktx',
args: ['--version'],
env: packageEnv,
},
{
label: 'published package global sl query',
command: 'ktx',
args: queryArgs,
env: runtimeEnv,
},
];
}
```
- [ ] **Step 4: Run the command-list test and verify it passes**
Run:
```bash
node --test scripts/published-package-smoke.test.mjs
```
Expected: PASS for the command construction tests, with remaining failures only
if the runner label validation test from Task 2 has already been added.
- [ ] **Step 5: Commit the command-builder change**
Run:
```bash
git add scripts/published-package-smoke-config.mjs scripts/published-package-smoke.test.mjs
git commit -m "test: cover published package runtime smoke commands"
```
### Task 2: Validate smoke runner labels for the new command list
**Files:**
- Modify: `scripts/published-package-smoke.test.mjs`
- Modify: `scripts/published-package-smoke.mjs`
- Test: `scripts/published-package-smoke.test.mjs`
- [ ] **Step 1: Write the failing label classification test**
In `scripts/published-package-smoke.test.mjs`, replace the import from
`./published-package-smoke.mjs` with this import:
```javascript
import {
buildPublishedPackageNpxCommand,
buildPublishedPackageSmokeCommands,
isPublishedPackageSemanticQueryLabel,
isPublishedPackageVersionLabel,
publishedPackageSpec,
readPublishedPackageSmokeConfig,
} from './published-package-smoke.mjs';
```
Add this test after the `describe('published package smoke command
construction', ...)` block:
```javascript
describe('published package smoke output validation labels', () => {
it('classifies version and semantic query commands', () => {
assert.equal(isPublishedPackageVersionLabel('published package npx version'), true);
assert.equal(isPublishedPackageVersionLabel('published package local version'), true);
assert.equal(isPublishedPackageVersionLabel('published package global version'), true);
assert.equal(isPublishedPackageVersionLabel('published package setup demo'), false);
assert.equal(isPublishedPackageSemanticQueryLabel('published package npx sl query'), true);
assert.equal(isPublishedPackageSemanticQueryLabel('published package local sl query'), true);
assert.equal(isPublishedPackageSemanticQueryLabel('published package global sl query'), true);
assert.equal(isPublishedPackageSemanticQueryLabel('published package local install'), false);
});
});
```
- [ ] **Step 2: Run the test and verify it fails**
Run:
```bash
node --test scripts/published-package-smoke.test.mjs
```
Expected: FAIL with an import error because
`isPublishedPackageSemanticQueryLabel` and `isPublishedPackageVersionLabel` are
not exported yet.
- [ ] **Step 3: Implement label classification and runner validation**
In `scripts/published-package-smoke.mjs`, add these constants and exports after
`const SMOKE_TIMEOUT_MS = 180_000;`:
```javascript
const VERSION_LABELS = new Set([
'published package npx version',
'published package local version',
'published package global version',
]);
const SEMANTIC_QUERY_LABELS = new Set([
'published package npx sl query',
'published package local sl query',
'published package global sl query',
]);
export function isPublishedPackageVersionLabel(label) {
return VERSION_LABELS.has(label);
}
export function isPublishedPackageSemanticQueryLabel(label) {
return SEMANTIC_QUERY_LABELS.has(label);
}
```
In `runPublishedPackageSmoke(config)`, replace this block:
```javascript
if (
command.label === 'published package version' ||
command.label === 'published package local binary' ||
command.label === 'published package global binary'
) {
assert.match(result.stdout, /@kaelio\/ktx /);
}
if (command.label === 'published package sl query') {
assert.match(result.stdout, /SELECT/i);
assert.match(result.stdout, /contracts/i);
}
```
with this block:
```javascript
if (isPublishedPackageVersionLabel(command.label)) {
assert.match(result.stdout, /@kaelio\/ktx /);
}
if (isPublishedPackageSemanticQueryLabel(command.label)) {
assert.match(result.stdout, /SELECT/i);
assert.match(result.stdout, /contracts/i);
}
```
- [ ] **Step 4: Run the label tests and verify they pass**
Run:
```bash
node --test scripts/published-package-smoke.test.mjs
```
Expected: PASS.
- [ ] **Step 5: Commit the runner-label change**
Run:
```bash
git add scripts/published-package-smoke.mjs scripts/published-package-smoke.test.mjs
git commit -m "test: validate published package smoke outputs"
```
### Task 3: Verify release-script compatibility
**Files:**
- Verify: `scripts/published-package-smoke-config.mjs`
- Verify: `scripts/published-package-smoke.mjs`
- Verify: `scripts/published-package-smoke.test.mjs`
- Verify: `scripts/release-readiness.test.mjs`
- Verify: `package.json`
- [ ] **Step 1: Run the focused Node tests**
Run:
```bash
node --test scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs
```
Expected: PASS. The release-readiness tests must continue to report the
published package smoke gate without executing the network smoke.
- [ ] **Step 2: Run release readiness**
Run:
```bash
pnpm run release:readiness
```
Expected: PASS and output containing these lines:
```text
Release mode: ci-artifact-only
NPM publish enabled: false
Published package smoke: pending
Published package smoke script: pnpm run release:published-smoke
```
- [ ] **Step 3: Confirm the network smoke stays explicit**
Run:
```bash
rg -n '"release:published-smoke": "node scripts/published-package-smoke.mjs --require-config"' package.json
```
Expected: PASS with one match in `package.json`. Do not run
`pnpm run release:published-smoke` in normal CI before the package is published
to the configured registry.
- [ ] **Step 4: Check pre-commit availability**
Run:
```bash
test ! -f .pre-commit-config.yaml
```
Expected: PASS in the current worktree. If a pre-commit config exists when this
plan is executed, run this instead after activating `.venv`:
```bash
source .venv/bin/activate
uv run pre-commit run --files scripts/published-package-smoke-config.mjs scripts/published-package-smoke.mjs scripts/published-package-smoke.test.mjs
```
- [ ] **Step 5: Commit verification-only fixes if needed**
If Step 1 or Step 2 required additional source changes, commit them with:
```bash
git add scripts/published-package-smoke-config.mjs scripts/published-package-smoke.mjs scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs package.json
git commit -m "chore: verify published package runtime smoke"
```
If no files changed after Task 2, do not create an empty commit.
## Acceptance criteria
- `buildPublishedPackageSmokeCommands()` derives
`<smoke root>/managed-runtime` from the demo project directory by default.
- Direct `npx @kaelio/ktx`, local `npx ktx`, and global `ktx` semantic query
commands all receive the same `KTX_RUNTIME_ROOT`.
- Local and global post-publication smoke coverage runs `sl query ... --yes`,
not only `--version`.
- `runPublishedPackageSmoke()` validates version output for all version labels
and validates generated SQL output for all semantic query labels.
- `node --test scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs`
passes.
- `pnpm run release:readiness` still reports the published-package smoke as a
pending explicit release gate while registry publishing is disabled.
## Self-review notes
- Spec coverage: this plan covers the remaining invocation-mode confidence gap
from the spec by proving the published package uses an isolated managed
runtime across direct `npx`, local binary, and global binary paths.
- Placeholder scan: the plan contains concrete file paths, exact code blocks,
exact commands, and exact expected outcomes.
- Type consistency: the command label strings are consistent across tests,
command construction, and smoke-runner output validation.

View file

@ -0,0 +1,978 @@
# Single Public Runtime Artifact Cleanup 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 release artifacts match the npm-managed Python runtime design:
one public `@kaelio/ktx` npm tarball plus one bundled `kaelio-ktx` runtime
wheel, with no standalone `ktx-sl` or `ktx-daemon` release artifacts.
**Architecture:** Keep `python/ktx-sl` and `python/ktx-daemon` as source
packages used to assemble the bundled runtime wheel. Remove direct standalone
Python wheel and source-distribution builds from the release artifact path,
manifest, readiness policy, and artifact smoke docs. The packed npm package
remains the only user-visible package; Python-backed verification continues
through the managed runtime installed from the bundled wheel.
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, uv-built bundled
runtime wheel, JSON release policy, Markdown.
---
## Current state
This plan follows
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
The following plan files are based on that spec and are implemented in the
current tree:
- `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`
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-prune-smoke-and-docs.md`
- `docs/superpowers/plans/2026-05-11-managed-runtime-uv-prerequisite-contract.md`
Implementation evidence found before writing this plan includes:
- `packages/cli/assets/python/manifest.json` and
`packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl`.
- `packages/cli/src/managed-python-runtime.ts`,
`packages/cli/src/managed-python-command.ts`,
`packages/cli/src/managed-python-daemon.ts`,
`packages/cli/src/managed-local-embeddings.ts`,
`packages/cli/src/managed-python-http.ts`, and `packages/cli/src/runtime.ts`.
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
`scripts/published-package-smoke.mjs`,
`scripts/local-embeddings-runtime-smoke.mjs`,
`scripts/publish-public-npm-package.mjs`, and
`.github/workflows/release.yml`.
- `release-policy.json` is in `npm-public-release-ready` mode, publishes
`@kaelio/ktx`, disables Python package publishing, and encodes the hard
`uv` prerequisite.
- `README.md` and `examples/package-artifacts/README.md` document public npm
usage, managed runtime commands, `runtime prune`, and the `uv` prerequisite.
The remaining mismatch is in the artifact release surface:
- `scripts/package-artifacts.mjs` still runs `uv build --package ktx-sl` and
`uv build --package ktx-daemon`.
- `scripts/package-artifacts.mjs` still adds `ktx-sl` and `ktx-daemon` wheel
and source-distribution files to the artifact manifest.
- `scripts/package-artifacts.mjs` still runs a direct Python clean-install
smoke, even though the npm artifact smoke already proves Python-backed
commands through the managed runtime.
- `release-policy.json` still lists `ktx-sl` and `ktx-daemon` under
`python.packages`.
- `examples/package-artifacts/README.md` says the Python smoke installs
standalone Python artifacts directly.
This plan removes those release artifacts. It does not delete the Python source
packages because the bundled runtime wheel builder still copies from
`python/ktx-sl/semantic_layer` and `python/ktx-daemon/src/ktx_daemon`.
## File structure
- Modify `scripts/package-artifacts.test.mjs`: make artifact tests expect only
`@kaelio/ktx` plus the `kaelio-ktx` bundled runtime wheel, and add a guard
that direct standalone Python artifact smoke code is gone.
- Modify `scripts/package-artifacts.mjs`: stop building standalone Python
artifacts, stop looking for their wheel and source-distribution files, remove
their release metadata, and remove the direct Python artifact verification
path.
- Modify `scripts/release-readiness.test.mjs`: update release policy fixtures
and readiness reports so the only Python release metadata is `kaelio-ktx`.
- Modify `release-policy.json`: set `python.packages` to `["kaelio-ktx"]`.
- Modify `scripts/examples-docs.test.mjs`: require docs to describe the single
npm tarball plus runtime wheel artifact shape and reject the old direct
Python-artifact smoke wording.
- Modify `README.md`: clarify that `python/ktx-sl` and `python/ktx-daemon` are
source packages, not release artifacts for the first npm release.
- Modify `examples/package-artifacts/README.md`: replace the stale standalone
Python smoke paragraph with the managed-runtime artifact contract.
### Task 1: Make package artifact tests expect one runtime wheel
**Files:**
- Modify: `scripts/package-artifacts.test.mjs`
- Test: `scripts/package-artifacts.test.mjs`
- [ ] **Step 1: Update package artifact imports**
In `scripts/package-artifacts.test.mjs`, replace the import from
`./package-artifacts.mjs` with this import:
```javascript
import {
CLI_PYTHON_ASSET_MANIFEST,
INTERNAL_NPM_WORKSPACE_PACKAGES,
RUNTIME_WHEEL_DISTRIBUTION_NAME,
RUNTIME_WHEEL_NORMALIZED_NAME,
RUNTIME_WHEEL_PACKAGE_VERSION,
artifactManifestPath,
buildArtifactCommands,
copyRuntimeWheelAssets,
findPythonArtifacts,
NPM_ARTIFACT_PACKAGES,
npmDemoSmokeSource,
npmRuntimeSmokeSource,
npmSmokePackageJson,
npmVerifySource,
packageArtifactLayout,
packageReleaseMetadata,
verifyArtifactManifest,
writeArtifactManifest,
} from './package-artifacts.mjs';
```
- [ ] **Step 2: Remove standalone Python fixture setup**
In `scripts/package-artifacts.test.mjs`, replace `writeReleaseMetadataInputs`
with this function:
```javascript
async function writeReleaseMetadataInputs(root) {
for (const packageInfo of INTERNAL_NPM_WORKSPACE_PACKAGES) {
await mkdir(join(root, packageInfo.packageRoot), { recursive: true });
await writeJson(join(root, packageInfo.packageRoot, 'package.json'), {
name: packageInfo.name,
version: '0.0.0-private',
private: true,
});
}
}
```
Replace `writeUploadableArtifactFixtures` with this function:
```javascript
async function writeUploadableArtifactFixtures(layout) {
await mkdir(layout.npmDir, { recursive: true });
await mkdir(layout.pythonDir, { recursive: true });
const fileContents = new Map([
...NPM_ARTIFACT_PACKAGES.map((packageInfo) => [
layout.npmTarballs[packageInfo.name],
`${packageInfo.name}-tarball`,
]),
[
join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
'kaelio-ktx-runtime-wheel',
],
]);
for (const [path, contents] of fileContents) {
await writeFile(path, contents);
}
}
```
- [ ] **Step 3: Change build command expectations**
In the `buildArtifactCommands` test, replace the body with this code:
```javascript
it('builds TypeScript packages and the runtime wheel before packing npm artifacts', () => {
const layout = packageArtifactLayout('/repo/ktx');
const commands = buildArtifactCommands(layout);
assert.deepEqual(
commands.slice(0, NPM_BUILD_PACKAGE_ORDER.length).map((command) => [command.command, command.args]),
NPM_BUILD_PACKAGE_ORDER.map((packageName) => ['pnpm', ['--filter', packageName, 'run', 'build']]),
);
assert.deepEqual(
commands.slice(NPM_BUILD_PACKAGE_ORDER.length, NPM_BUILD_PACKAGE_ORDER.length + 1).map((command) => [
command.command,
command.args,
]),
[[process.execPath, ['scripts/build-python-runtime-wheel.mjs']]],
);
assert.deepEqual(
commands.slice(NPM_BUILD_PACKAGE_ORDER.length + 1).map((command) => [command.command, command.args]),
[[process.execPath, ['scripts/build-public-npm-package.mjs']]],
);
});
```
- [ ] **Step 4: Change release metadata expectations**
In the `packageReleaseMetadata` test, replace the expected array with this
array:
```javascript
assert.deepEqual(await packageReleaseMetadata(root), [
{
ecosystem: 'npm',
packageName: '@kaelio/ktx',
packageRoot: 'packages/cli',
packageVersion: '0.1.0',
private: false,
releaseMode: 'ci-artifact-only',
},
{
ecosystem: 'python',
packageName: 'kaelio-ktx',
packageRoot: 'python/runtime-wheel',
packageVersion: '0.1.0',
private: false,
releaseMode: 'ci-artifact-only',
},
]);
```
- [ ] **Step 5: Change Python artifact discovery expectations**
Replace the `findPythonArtifacts` success test with this test:
```javascript
it('finds the bundled runtime wheel only', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-artifacts-test-'));
try {
await writeFile(join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'), '');
assert.deepEqual(await findPythonArtifacts(root), {
runtimeWheel: join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
```
- [ ] **Step 6: Change artifact manifest expectations**
Inside the artifact manifest test, replace the Python package assertion with:
```javascript
assert.deepEqual(
manifest.packages.filter((entry) => entry.ecosystem === 'python'),
[
{
ecosystem: 'python',
packageName: 'kaelio-ktx',
packageRoot: 'python/runtime-wheel',
packageVersion: '0.1.0',
private: false,
releaseMode: 'ci-artifact-only',
},
],
);
```
Replace the Python file assertion with:
```javascript
assert.deepEqual(
manifest.files
.filter((file) => file.ecosystem === 'python')
.map((file) => ({
artifactKind: file.artifactKind,
ecosystem: file.ecosystem,
packageName: file.packageName,
packageVersion: file.packageVersion,
path: file.path,
})),
[
{
artifactKind: 'wheel',
ecosystem: 'python',
packageName: 'kaelio-ktx',
packageVersion: '0.1.0',
path: 'python/kaelio_ktx-0.1.0-py3-none-any.whl',
},
],
);
```
In the `verifyArtifactManifest` success test, replace the file-count assertion
with:
```javascript
assert.equal(manifest.files.length, NPM_ARTIFACT_PACKAGES.length + 1);
```
- [ ] **Step 7: Replace direct Python smoke tests with a dead-code guard**
Remove the whole `describe('pythonArtifactInstallArgs', ...)` block.
In `describe('verification snippets', ...)`, remove the test named
`asserts the Python modules that clean installs must expose`.
Add this test after the `verifyNpmArtifacts` test:
```javascript
describe('standalone Python artifact cleanup', () => {
it('does not build or verify standalone Python package artifacts', async () => {
const source = await readFile(new URL('./package-artifacts.mjs', import.meta.url), 'utf8');
assert.doesNotMatch(source, /uv', \['build', '--package', 'ktx-sl'/);
assert.doesNotMatch(source, /uv', \['build', '--package', 'ktx-daemon'/);
assert.doesNotMatch(source, /async function verifyPythonArtifacts/);
assert.doesNotMatch(source, /pythonArtifactInstallArgs/);
assert.doesNotMatch(source, /pythonVerifySource/);
assert.doesNotMatch(source, /ktx_sl-0\.1\.0/);
assert.doesNotMatch(source, /ktx_daemon-0\.1\.0/);
});
});
```
- [ ] **Step 8: Run package artifact tests and verify failure**
Run:
```bash
node --test scripts/package-artifacts.test.mjs
```
Expected: FAIL. The failures mention the extra `ktx-sl` and `ktx-daemon`
artifact commands, metadata entries, manifest files, or direct Python smoke
helpers.
### Task 2: Remove standalone Python artifacts from package artifacts
**Files:**
- Modify: `scripts/package-artifacts.mjs`
- Test: `scripts/package-artifacts.test.mjs`
- [ ] **Step 1: Remove dead constants and imports**
In `scripts/package-artifacts.mjs`, replace the `node:path` import with this
import:
```javascript
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
```
Remove these constants:
```javascript
const PACKAGE_VERSION = '0.0.0-private';
const PYTHON_PACKAGE_VERSION = '0.1.0';
```
Remove the whole `ordersSource` constant block.
- [ ] **Step 2: Make npm artifact names public-package only**
Replace `npmPackageTarballName` with this function:
```javascript
function npmPackageTarballName(packageName) {
if (packageName !== PUBLIC_NPM_PACKAGE_NAME) {
throw new Error(`Unsupported npm artifact package: ${packageName}`);
}
return publicNpmPackageTarballName(PUBLIC_NPM_PACKAGE_VERSION);
}
```
- [ ] **Step 3: Remove standalone Python build commands**
Replace `buildArtifactCommands` with this function:
```javascript
export function buildArtifactCommands(layout) {
const packagesByName = new Map(INTERNAL_NPM_WORKSPACE_PACKAGES.map((packageInfo) => [packageInfo.name, packageInfo]));
const npmBuildCommands = NPM_ARTIFACT_BUILD_ORDER.map((packageName) => {
const packageInfo = packagesByName.get(packageName);
if (!packageInfo) {
throw new Error(`Unknown npm artifact build package: ${packageName}`);
}
return {
command: 'pnpm',
args: ['--filter', packageInfo.name, 'run', 'build'],
cwd: layout.rootDir,
};
});
const publicPackageCommand = {
command: process.execPath,
args: ['scripts/build-public-npm-package.mjs'],
cwd: layout.rootDir,
};
return [
...npmBuildCommands,
{
command: process.execPath,
args: ['scripts/build-python-runtime-wheel.mjs'],
cwd: layout.rootDir,
},
publicPackageCommand,
];
}
```
- [ ] **Step 4: Discover only the bundled runtime wheel**
Replace `findOne` and `findPythonArtifacts` with these functions:
```javascript
function findOne(files, distributionName, suffix, label, pythonDir, version) {
const normalized = normalizePythonDistributionName(distributionName);
const found = files.find((file) => file.startsWith(`${normalized}-${version}`) && file.endsWith(suffix));
if (!found) {
throw new Error(`Missing Python artifact: ${label}`);
}
return join(pythonDir, found);
}
export async function findPythonArtifacts(pythonDir) {
const files = await readdir(pythonDir);
return {
runtimeWheel: findOne(
files,
RUNTIME_WHEEL_DISTRIBUTION_NAME,
'.whl',
'kaelio-ktx runtime wheel',
pythonDir,
RUNTIME_WHEEL_PACKAGE_VERSION,
),
};
}
```
- [ ] **Step 5: Emit release metadata only for npm and runtime wheel**
Replace `packageReleaseMetadata` with this function:
```javascript
export async function packageReleaseMetadata(rootDir = scriptRootDir()) {
const npmPackages = await Promise.all(
NPM_ARTIFACT_PACKAGES.map((packageInfo) => readNpmPackageMetadata(rootDir, packageInfo)),
);
return [
...npmPackages,
releaseMetadataEntry({
ecosystem: 'python',
packageName: RUNTIME_WHEEL_DISTRIBUTION_NAME,
packageRoot: 'python/runtime-wheel',
packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION,
privatePackage: false,
}),
];
}
```
- [ ] **Step 6: Remove dead TOML metadata helpers**
Delete these helper functions from `scripts/package-artifacts.mjs` because
release metadata no longer reads standalone Python `pyproject.toml` files:
```javascript
function readProjectBlock(toml, sourcePath) {
const lines = toml.split(/\r?\n/);
const block = [];
let inProject = false;
for (const line of lines) {
if (/^\[project\]\s*$/.test(line)) {
inProject = true;
continue;
}
if (inProject && /^\[.*\]\s*$/.test(line)) {
break;
}
if (inProject) {
block.push(line);
}
}
if (!inProject) {
throw new Error(`Missing [project] table in ${sourcePath}`);
}
return block.join('\n');
}
```
```javascript
function readTomlStringField(projectBlock, fieldName, sourcePath) {
const match = projectBlock.match(new RegExp(`^${fieldName}\\s*=\\s*"([^"]+)"\\s*$`, 'm'));
if (!match) {
throw new Error(`Missing project.${fieldName} in ${sourcePath}`);
}
return match[1];
}
```
```javascript
async function readPyprojectMetadata(path) {
const toml = await readFile(path, 'utf-8');
const projectBlock = readProjectBlock(toml, path);
return {
name: readTomlStringField(projectBlock, 'name', path),
version: readTomlStringField(projectBlock, 'version', path),
};
}
```
- [ ] **Step 7: Emit manifest records only for npm and runtime wheel**
Replace `artifactPackageRecords` with this function:
```javascript
function artifactPackageRecords(layout, pythonArtifacts, packages) {
const packagesByName = packageMetadataByName(packages);
const npmRecords = NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({
artifactKind: 'tarball',
artifactPath: layout.npmTarballs[packageInfo.name],
metadata: requirePackageMetadata(packagesByName, packageInfo.name),
}));
return [
...npmRecords,
{
artifactKind: 'wheel',
artifactPath: pythonArtifacts.runtimeWheel,
metadata: requirePackageMetadata(packagesByName, RUNTIME_WHEEL_DISTRIBUTION_NAME),
},
];
}
```
- [ ] **Step 8: Remove direct Python artifact verification helpers**
Delete these exports and functions from `scripts/package-artifacts.mjs`:
```javascript
export function pythonArtifactInstallArgs(python, pythonArtifacts) {
return ['pip', 'install', '--python', python, pythonArtifacts.runtimeWheel];
}
```
```javascript
export function pythonVerifySource() {
return `
import importlib.metadata
import semantic_layer
import ktx_daemon
assert importlib.metadata.version("kaelio-ktx") == "0.1.0"
assert semantic_layer is not None
assert ktx_daemon.PACKAGE_NAME == "ktx-daemon"
`;
}
```
```javascript
function pythonExecutable(projectDir) {
if (process.platform === 'win32') {
return join(projectDir, '.venv', 'Scripts', 'python.exe');
}
return join(projectDir, '.venv', 'bin', 'python');
}
```
```javascript
export function npmSmokePythonEnv(projectDir, baseEnv = process.env) {
const binDir = process.platform === 'win32' ? join(projectDir, '.venv', 'Scripts') : join(projectDir, '.venv', 'bin');
const existingPath = baseEnv.PATH ?? '';
return {
...baseEnv,
PATH: existingPath ? `${binDir}${delimiter}${existingPath}` : binDir,
};
}
```
```javascript
async function verifyPythonArtifacts(layout, tmpRoot) {
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
const projectDir = join(tmpRoot, 'python-clean-install');
await mkdir(projectDir, { recursive: true });
const python = pythonExecutable(projectDir);
await writeFile(join(projectDir, 'verify_python.py'), pythonVerifySource());
await runCommand('uv', ['venv', '.venv'], { cwd: projectDir });
await runCommand('uv', pythonArtifactInstallArgs(python, pythonArtifacts), {
cwd: projectDir,
});
await runCommand(python, ['verify_python.py'], { cwd: projectDir });
await runCommand(python, ['-m', 'ktx_daemon', 'semantic-validate'], {
cwd: projectDir,
input: `${JSON.stringify({ sources: [ordersSource], dialect: 'postgres' })}\n`,
});
}
```
- [ ] **Step 9: Verify artifacts through npm only**
Replace `verifyArtifacts` with this function:
```javascript
async function verifyArtifacts(layout) {
await verifyArtifactManifest(layout);
const tmpRoot = await mkdtemp(join(tmpdir(), 'ktx-artifacts-'));
try {
await verifyNpmArtifacts(layout, tmpRoot);
} finally {
await rm(tmpRoot, { recursive: true, force: true });
}
}
```
- [ ] **Step 10: Run package artifact tests and verify pass**
Run:
```bash
node --test scripts/package-artifacts.test.mjs
```
Expected: PASS. The output includes `# fail 0`.
- [ ] **Step 11: Commit package artifact cleanup**
Run:
```bash
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
git commit -m "refactor: limit release artifacts to public package runtime"
```
### Task 3: Align release policy and readiness reports
**Files:**
- Modify: `release-policy.json`
- Modify: `scripts/release-readiness.test.mjs`
- Test: `scripts/release-readiness.test.mjs`
- [ ] **Step 1: Update release readiness fixtures**
In `scripts/release-readiness.test.mjs`, replace
`writeReleaseMetadataInputs` with:
```javascript
async function writeReleaseMetadataInputs(root) {
for (const packageInfo of INTERNAL_NPM_WORKSPACE_PACKAGES) {
await mkdir(join(root, packageInfo.packageRoot), { recursive: true });
await writeJson(join(root, packageInfo.packageRoot, 'package.json'), {
name: packageInfo.name,
version: '0.0.0-private',
private: true,
});
}
}
```
Replace `writeUploadableArtifactFixtures` with:
```javascript
async function writeUploadableArtifactFixtures(layout) {
await mkdir(layout.npmDir, { recursive: true });
await mkdir(layout.pythonDir, { recursive: true });
const fileContents = new Map([
...NPM_ARTIFACT_PACKAGES.map((packageInfo) => [
layout.npmTarballs[packageInfo.name],
`${packageInfo.name}-tarball`,
]),
[join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'],
]);
for (const [path, contents] of fileContents) {
await writeFile(path, contents);
}
}
```
In `releasePolicy`, replace the `python` object with:
```javascript
python: {
publish: false,
repository: null,
packages: ['kaelio-ktx'],
...pythonOverrides,
},
```
- [ ] **Step 2: Update readiness report expectations**
In `scripts/release-readiness.test.mjs`, replace every expected
`packageNames` array with:
```javascript
packageNames: ['@kaelio/ktx', 'kaelio-ktx'],
```
There are three report assertions to update:
- `accepts the current ci-artifact-only policy, package metadata, and artifact manifest`
- `reports required published package smoke when release mode requires it`
- `accepts the npm public release ready policy`
- [ ] **Step 3: Update checked release policy**
In `release-policy.json`, replace the `python.packages` value with:
```json
"packages": ["kaelio-ktx"]
```
- [ ] **Step 4: Run readiness tests and verify pass**
Run:
```bash
node --test scripts/release-readiness.test.mjs
```
Expected: PASS. The output includes `# fail 0`.
- [ ] **Step 5: Commit release policy cleanup**
Run:
```bash
git add release-policy.json scripts/release-readiness.test.mjs
git commit -m "chore: align release policy with bundled runtime wheel"
```
### Task 4: Document the single release artifact surface
**Files:**
- Modify: `scripts/examples-docs.test.mjs`
- Modify: `README.md`
- Modify: `examples/package-artifacts/README.md`
- Test: `scripts/examples-docs.test.mjs`
- [ ] **Step 1: Add failing docs assertions**
In `scripts/examples-docs.test.mjs`, inside
`it('documents the public package artifact smoke shape', ...)`, add these
assertions after the existing `assert.match(readme, /managed Python runtime/);`
line:
```javascript
assert.match(readme, /public `@kaelio\/ktx` npm tarball and the bundled `kaelio-ktx` runtime wheel/);
assert.match(readme, /does not install standalone Python packages directly/);
assert.doesNotMatch(readme, /standalone Python distributions/);
assert.doesNotMatch(readme, /installs the Python artifacts directly/);
```
In `it('documents public npm and managed runtime usage in the README', ...)`,
add these assertions after the existing `uv` assertions:
```javascript
assert.match(rootReadme, /release artifact manifest contains the public npm tarball and the bundled `kaelio-ktx` runtime wheel/);
assert.match(rootReadme, /source packages for development, not public release artifacts/);
```
- [ ] **Step 2: Run docs tests and verify failure**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: FAIL. The failure mentions the missing single-artifact wording in
`README.md` or `examples/package-artifacts/README.md`.
- [ ] **Step 3: Update the package artifact example README**
In `examples/package-artifacts/README.md`, replace:
```markdown
The Python smoke project still installs the Python artifacts directly because
it verifies the standalone Python distributions that feed the bundled runtime
wheel.
```
with:
```markdown
The artifact manifest contains the public `@kaelio/ktx` npm tarball and the
bundled `kaelio-ktx` runtime wheel. The smoke does not install standalone
Python packages directly; Python-backed behavior is verified through the
managed runtime installed from the npm package.
```
- [ ] **Step 4: Update the root README release status**
In `README.md`, in the `## Release status` section, replace this paragraph:
```markdown
This repository builds one public npm artifact named `@kaelio/ktx`. The first
public npm handoff is policy-gated through `release-policy.json`, which keeps
Python package publishing disabled because KTX-owned Python code ships inside
the npm package as a bundled wheel.
```
with:
```markdown
This repository builds one public npm artifact named `@kaelio/ktx`. The release
artifact manifest contains the public npm tarball and the bundled `kaelio-ktx`
runtime wheel. The first public npm handoff is policy-gated through
`release-policy.json`, which keeps Python package publishing disabled because
KTX-owned Python code ships inside the npm package as a bundled wheel. The
`python/ktx-sl` and `python/ktx-daemon` directories remain source packages for
development, not public release artifacts.
```
- [ ] **Step 5: Run docs tests and verify pass**
Run:
```bash
node --test scripts/examples-docs.test.mjs
```
Expected: PASS. The output includes `# fail 0`.
- [ ] **Step 6: Commit docs cleanup**
Run:
```bash
git add README.md examples/package-artifacts/README.md scripts/examples-docs.test.mjs
git commit -m "docs: describe single public runtime artifact surface"
```
### Task 5: Verify the cleaned release artifact contract
**Files:**
- Verify: `scripts/package-artifacts.mjs`
- Verify: `scripts/package-artifacts.test.mjs`
- Verify: `scripts/release-readiness.test.mjs`
- Verify: `scripts/examples-docs.test.mjs`
- Verify: `release-policy.json`
- Verify: `README.md`
- Verify: `examples/package-artifacts/README.md`
- [ ] **Step 1: Run focused tests**
Run:
```bash
node --test scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs
```
Expected: PASS. The output includes `# fail 0`.
- [ ] **Step 2: Verify stale artifact strings are gone from production/docs files**
Run (scans only production and docs files, not test files — test files keep guard assertions that reference the removed strings):
```bash
rg -n "uv', \\['build', '--package', 'ktx-sl'|uv', \\['build', '--package', 'ktx-daemon'|ktx_sl-0\\.1\\.0|ktx_daemon-0\\.1\\.0|pythonArtifactInstallArgs|pythonVerifySource|verifyPythonArtifacts|standalone Python distributions|installs the Python artifacts directly" scripts/package-artifacts.mjs scripts/release-readiness.mjs README.md examples/package-artifacts/README.md release-policy.json
```
Expected: no matches.
- [ ] **Step 3: Verify release readiness against the current artifact manifest**
Run:
```bash
pnpm run release:readiness -- --json
```
Expected: PASS when `dist/artifacts/manifest.json` has been rebuilt after this
change. The JSON output contains:
```json
{
"releaseMode": "npm-public-release-ready",
"packageNames": ["@kaelio/ktx", "kaelio-ktx"],
"pythonPublishEnabled": false
}
```
If this command fails because the local artifact manifest was generated before
the cleanup, run:
```bash
pnpm run artifacts:check
pnpm run release:readiness -- --json
```
Expected: both commands pass. The rebuilt manifest contains only
`npm/kaelio-ktx-0.1.0.tgz` and
`python/kaelio_ktx-0.1.0-py3-none-any.whl` under `files`.
- [ ] **Step 4: Run pre-commit on changed files when configured**
Run:
```bash
uv run pre-commit run --files scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs release-policy.json README.md examples/package-artifacts/README.md
```
Expected: PASS. If pre-commit is not installed or no pre-commit config exists,
record the exact error and keep the focused Node test output from Step 1.
- [ ] **Step 5: Commit final verification fixes if needed**
If Step 1, Step 2, Step 3, or Step 4 required code or docs fixes, commit them:
```bash
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs release-policy.json README.md examples/package-artifacts/README.md
git commit -m "test: verify single public runtime artifact contract"
```
If no fixes were required after the previous commits, do not create an empty
commit.
## Acceptance criteria
- `scripts/package-artifacts.mjs` builds TypeScript packages, builds the
bundled `kaelio-ktx` runtime wheel, copies it into CLI assets, and packs the
public `@kaelio/ktx` npm tarball.
- `scripts/package-artifacts.mjs` no longer builds `ktx-sl` or `ktx-daemon`
standalone wheel or source-distribution artifacts.
- Artifact manifests contain release metadata for `@kaelio/ktx` and
`kaelio-ktx` only.
- `release-policy.json` lists only `@kaelio/ktx` under `npm.packages` and only
`kaelio-ktx` under `python.packages`.
- The artifact smoke verifies Python-backed behavior through the installed
public npm package and managed runtime, not by installing standalone Python
artifacts directly.
- Public docs state that `python/ktx-sl` and `python/ktx-daemon` remain source
packages for development, not public release artifacts.
## Self-review
Spec coverage:
- The plan preserves the single public npm package requirement.
- The plan preserves the bundled KTX-owned Python wheel requirement.
- The plan keeps Python package publishing disabled.
- The plan removes the only remaining artifact path that treated KTX-owned
Python source packages as standalone release artifacts.
Placeholder scan:
- No steps contain placeholder implementation text.
- Every code-changing step names exact files and provides concrete replacement
snippets.
Type and name consistency:
- Public npm package name remains `@kaelio/ktx`.
- Bundled runtime distribution name remains `kaelio-ktx`.
- Runtime wheel filename remains `kaelio_ktx-0.1.0-py3-none-any.whl`.
- Removed standalone Python artifact names are consistently `ktx-sl` and
`ktx-daemon`.

View file

@ -0,0 +1,234 @@
# npm-managed Python runtime design
This spec defines how KTX ships as one visible npm package while still using
Python for sqlglot, semantic-layer planning, database-agent compute, and local
embeddings. The goal is a user experience where users install or run only
`@kaelio/ktx`, and KTX manages its Python runtime automatically when a command
needs it.
## Goals
KTX must be usable through the npm package `@kaelio/ktx` with a `ktx` binary.
Users can run KTX without learning about the Python packages that power parts of
the system.
The first release must support these invocation modes:
- `npx @kaelio/ktx setup demo`
- `npx @kaelio/ktx sl query ...`
- `npm install @kaelio/ktx`, followed by `npx ktx ...`
- `npm install -g @kaelio/ktx`, followed by `ktx ...`
KTX-owned Python code must ship inside the npm package as a bundled wheel. KTX
doesn't need to publish its own Python code to PyPI for this release.
## Non-goals
This release does not need to provide a public TypeScript SDK split across
multiple npm packages. The internal workspace package layout can remain useful
for development, but the public npm surface is a single package.
This release does not need a fully offline install. KTX's own Python wheel is
bundled, but third-party Python dependencies can come from PyPI through `uv`.
This release does not install local embedding dependencies by default. Local
embeddings remain lazy because `sentence-transformers`, `torch`, and model
downloads are large.
## Package model
KTX publishes one public npm package:
```text
@kaelio/ktx
```
That package exposes one binary:
```json
{
"bin": {
"ktx": "./dist/bin.js"
}
}
```
The npm package includes these assets:
- Bundled JavaScript CLI output.
- Packaged demo assets.
- One KTX-owned Python wheel, for example
`python/kaelio_ktx-0.1.0-py3-none-any.whl`.
- A wheel checksum or runtime manifest that lets the CLI verify the bundled
Python payload before installation.
The Python wheel contains the current `semantic_layer` and `ktx_daemon`
modules. It exposes at least the `ktx-daemon` console script.
## Runtime installation
KTX creates a managed Python runtime only when a command needs Python-backed
behavior. The runtime lives outside the npm cache so it survives `npx` runs.
The runtime root is platform-specific:
- macOS: `~/Library/Application Support/kaelio/ktx/runtime`
- Linux: `${XDG_DATA_HOME:-~/.local/share}/kaelio/ktx/runtime`
- Windows: `%LOCALAPPDATA%/Kaelio/KTX/runtime`
The runtime is versioned by the npm package version. A versioned runtime avoids
mixing JavaScript and Python code from incompatible releases.
The installer performs these steps:
1. Locate `uv`.
2. Create a virtual environment under the versioned runtime directory.
3. Install the bundled KTX wheel into that environment.
4. Write a runtime manifest with the CLI version, wheel checksum, Python
executable, daemon executable, and installed feature set.
For lightweight Python support, the install command uses the bundled wheel's
default dependency set. For local embeddings, the installer adds the embeddings
extra only when selected:
```bash
uv pip install "/path/to/kaelio_ktx-0.1.0-py3-none-any.whl"
uv pip install "/path/to/kaelio_ktx-0.1.0-py3-none-any.whl[local-embeddings]"
```
## Feature installation levels
KTX manages Python runtime features in levels so first use stays fast.
`core` includes:
- `sqlglot`
- `pydantic`
- `pyyaml`
- `fastapi`
- `uvicorn`
- lightweight daemon dependencies
`local-embeddings` adds:
- `sentence-transformers`
- `torch`
- model download support for `all-MiniLM-L6-v2`
Commands that only need semantic-layer SQL generation require `core`.
Commands that need local embeddings require `local-embeddings`.
## Command behavior
Pure TypeScript commands run without the managed Python runtime.
Python-backed one-shot operations use the managed `ktx-daemon` executable
directly. Examples include semantic query compilation, semantic validation,
semantic source generation, and sqlglot-backed table identifier parsing.
Repeated or expensive operations use a managed HTTP daemon. Local embeddings use
the daemon because loading the model for every one-shot process is too slow.
KTX provides runtime management commands:
```bash
ktx runtime install
ktx runtime status
ktx runtime start
ktx runtime stop
ktx runtime doctor
ktx runtime prune
```
Normal commands can install the runtime lazily. Runtime commands make that
behavior inspectable and debuggable.
## Daemon lifecycle
The daemon binds to `127.0.0.1` on an available random port. KTX writes daemon
state to the runtime manifest or an adjacent state file:
```json
{
"pid": 12345,
"port": 58731,
"version": "0.1.0",
"features": ["core", "local-embeddings"],
"startedAt": "2026-05-11T00:00:00Z"
}
```
Before reusing a daemon, KTX checks that the process is alive, the port responds
to `/health`, and the daemon version matches the CLI version. If any check
fails, KTX treats the daemon as stale and starts a new one.
KTX uses one-shot Python for short operations by default. It starts the daemon
only when a command benefits from process reuse.
## Interactive and CI behavior
In an interactive terminal, KTX prompts before installing the managed runtime
for the first time. The prompt states that Python dependencies will be
downloaded.
With `--yes`, KTX installs the required runtime features without prompting.
With `--no-input`, KTX fails if a required runtime feature is missing and no
explicit auto-install flag is present. The error prints the exact command to
prepare the runtime.
For local embeddings, KTX prompts separately because the dependency and model
downloads are larger than the core runtime.
## Error handling
If `uv` is missing, KTX prints a focused error that explains how to install it
and how to retry. A later release can add a bundled or downloaded `uv` strategy.
If Python runtime installation fails, KTX preserves install logs in the runtime
directory and prints the log path.
If the daemon fails to start, KTX prints the captured daemon stdout and stderr
path. It falls back to one-shot mode only when the requested operation supports
one-shot execution.
If JavaScript and Python versions don't match, KTX reinstalls the managed
runtime for the current npm package version.
## Release flow
The release builds the Python wheel before packing npm artifacts. The npm pack
step includes the wheel as an asset.
Release checks must cover:
1. Clean install of the packed npm package.
2. `npx` execution of the packed package.
3. First-run managed runtime install from the bundled wheel.
4. One-shot semantic-layer query through the managed runtime.
5. Runtime status and doctor output.
6. Daemon start, health check, reuse, and stop.
7. Optional local embeddings smoke in a separate job or opt-in check.
## Open decisions
KTX still needs a final decision on whether `uv` is a hard prerequisite or a
bootstrap dependency that KTX downloads automatically.
KTX also needs the final Python distribution name. This spec uses
`kaelio-ktx` as the distribution name and `kaelio_ktx` in wheel filenames.
## Success criteria
Users can run `npx @kaelio/ktx ...` and complete Python-backed KTX operations
without manually installing a KTX Python package.
Users who install `@kaelio/ktx` locally can run `npx ktx ...` through the local
project's npm binary resolution.
The first Python-backed command installs only the core runtime. Local embedding
dependencies install only after the user selects local embeddings or explicitly
requests the `local-embeddings` runtime feature.
KTX can diagnose and repair stale or mismatched managed runtimes without asking
users to delete directories manually.