* 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
39 KiB
Public NPM Release Handoff 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: Turn the remaining npm-managed Python runtime release gap into a
guarded public @kaelio/ktx npm release handoff for version 0.1.0.
Architecture: Keep one public npm package and keep Python packages
unpublished. The public package builder stamps the assembled @kaelio/ktx
package as 0.1.0, release readiness accepts a publish-ready policy only when
all blocking decisions are encoded, and a new publish script performs a dry-run
by default before any live registry publish.
Tech Stack: Node 22 ESM scripts, node:test, pnpm 10 publish, JSON release
policy, GitHub Actions workflow validation.
Spec trace and current state
This plan follows
docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md.
The existing plan files that reference that spec are:
docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.mddocs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.mddocs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.mddocs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.mddocs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.mddocs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.mddocs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.mddocs/superpowers/plans/2026-05-11-managed-python-runtime-installer.mddocs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.mddocs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.mddocs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.mddocs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md
All twelve are implemented in the current tree: their referenced source and test files exist, and the runtime command, daemon, package artifact, published-package smoke, local-embedding smoke, and README markers are present.
The remaining release gap is explicit in release-policy.json: the repository
still uses ci-artifact-only, npm.publish is false, and the README states
that registry publishing is disabled. This plan changes that to a guarded
handoff for the first public npm release while leaving Python registry
publication disabled because the spec says KTX-owned Python code ships inside
the npm package as a bundled wheel for this release.
File structure
- Modify
scripts/build-public-npm-package.mjs: make the assembled public npm package version and tarball name0.1.0instead of0.0.0-private. - Modify
scripts/build-public-npm-package.test.mjs: cover public version stamping and the versioned tarball path. - Modify
scripts/package-artifacts.mjs: make artifact metadata report@kaelio/ktxas version0.1.0. - Modify
scripts/package-artifacts.test.mjs: update artifact manifest, metadata, runtime smoke, and demo smoke expectations for the public tarball. - Modify
scripts/local-embeddings-runtime-smoke.test.mjs: update public tarball selection coverage forkaelio-ktx-0.1.0.tgz. - Modify
scripts/release-readiness.mjs: add thenpm-public-release-readyrelease mode and policy validation. - Modify
scripts/release-readiness.test.mjs: cover the publish-ready policy and validation failures. - Modify
release-policy.json: encode the first public npm release handoff. - Create
scripts/publish-public-npm-package.mjs: verify readiness and runpnpm publishin dry-run mode by default. - Create
scripts/publish-public-npm-package.test.mjs: cover publish command construction and policy gating. - Modify
package.json: addrelease:npm-publish. - Create
.github/workflows/release.yml: add a manual dry-run/live publish workflow for the public npm tarball. - Create
scripts/release-workflow.test.mjs: validate that the release workflow is manual, uses pnpm, runs readiness checks, and gates live publish. - Modify
README.md: replace the disabled publishing note with the guarded handoff commands.
Task 1: Stamp public npm artifacts as 0.1.0
Files:
-
Modify:
scripts/build-public-npm-package.mjs -
Modify:
scripts/build-public-npm-package.test.mjs -
Modify:
scripts/package-artifacts.mjs -
Modify:
scripts/package-artifacts.test.mjs -
Modify:
scripts/local-embeddings-runtime-smoke.test.mjs -
Step 1: Write failing public version tests
In scripts/build-public-npm-package.test.mjs, extend the import from
./build-public-npm-package.mjs so it includes PUBLIC_NPM_PACKAGE_VERSION
and publicNpmPackageTarballName:
import {
PUBLIC_BUNDLED_WORKSPACE_PACKAGES,
PUBLIC_NPM_PACKAGE_NAME,
PUBLIC_NPM_PACKAGE_VERSION,
collectPublicDependencies,
createPublicNpmPackageTree,
publicNpmPackageJson,
publicNpmPackageLayout,
publicNpmPackageTarballName,
publicNpmPackCommand,
} from './build-public-npm-package.mjs';
Replace the publicNpmPackageLayout test expectation with:
describe('publicNpmPackageLayout', () => {
it('uses the first public npm release version for the tarball name', () => {
const layout = publicNpmPackageLayout('/repo/ktx');
assert.equal(PUBLIC_NPM_PACKAGE_VERSION, '0.1.0');
assert.equal(publicNpmPackageTarballName(), 'kaelio-ktx-0.1.0.tgz');
assert.equal(layout.tarballPath, '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz');
});
});
In the publicNpmPackageJson test, add this assertion after the package name
assertion:
assert.equal(packageJson.version, '0.1.0');
In the publicNpmPackCommand test, replace the tarball assertion block with:
assert.deepEqual(publicNpmPackCommand(layout), {
command: 'pnpm',
args: [
'--config.node-linker=hoisted',
'pack',
'--out',
'/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz',
],
cwd: '/repo/ktx/dist/public-npm-package',
});
- Step 2: Run public package tests to verify failure
Run:
node --test scripts/build-public-npm-package.test.mjs
Expected: FAIL. The failure mentions at least one stale
kaelio-ktx-0.0.0-private.tgz or 0.0.0-private public package version
expectation.
- Step 3: Implement public version stamping
In scripts/build-public-npm-package.mjs, replace the current public version
constants and layout helper with:
export const PUBLIC_NPM_PACKAGE_NAME = '@kaelio/ktx';
export const PUBLIC_NPM_PACKAGE_VERSION = '0.1.0';
export function publicNpmPackageTarballName(version = PUBLIC_NPM_PACKAGE_VERSION) {
return `kaelio-ktx-${version}.tgz`;
}
Replace publicNpmPackageLayout with:
export function publicNpmPackageLayout(rootDir = scriptRootDir(), version = PUBLIC_NPM_PACKAGE_VERSION) {
return {
rootDir,
packageVersion: version,
cliPackageRoot: join(rootDir, 'packages', 'cli'),
packRoot: join(rootDir, 'dist', 'public-npm-package'),
npmDir: join(rootDir, 'dist', 'artifacts', 'npm'),
tarballPath: join(rootDir, 'dist', 'artifacts', 'npm', publicNpmPackageTarballName(version)),
};
}
Change publicNpmPackageJson so it accepts the public version explicitly:
export function publicNpmPackageJson(cliPackageJson, dependencies, version = PUBLIC_NPM_PACKAGE_VERSION) {
return {
name: PUBLIC_NPM_PACKAGE_NAME,
version,
description: 'Standalone KTX context layer for database agents',
private: false,
type: 'module',
engines: cliPackageJson.engines ?? { node: '>=22.0.0' },
bin: { ktx: './dist/bin.js' },
main: cliPackageJson.main ?? 'dist/index.js',
types: cliPackageJson.types ?? 'dist/index.d.ts',
exports: cliPackageJson.exports ?? {
'.': {
types: './dist/index.d.ts',
import: './dist/index.js',
default: './dist/index.js',
},
'./package.json': './package.json',
},
files: ['dist', 'assets'],
dependencies,
bundledDependencies: PUBLIC_BUNDLED_WORKSPACE_PACKAGES,
license: cliPackageJson.license ?? 'Apache-2.0',
repository: {
type: 'git',
url: 'git+https://github.com/kaelio/ktx.git',
},
bugs: {
url: 'https://github.com/kaelio/ktx/issues',
},
homepage: 'https://github.com/kaelio/ktx#readme',
};
}
In copyCliPackage, pass the layout version:
await writeJson(
join(layout.packRoot, 'package.json'),
publicNpmPackageJson(cliPackageJson, dependencies, layout.packageVersion),
);
In createPublicNpmPackageTree, return the versioned package JSON:
return {
layout,
packageJson: publicNpmPackageJson(cliPackageJson, dependencies, layout.packageVersion),
bundledPackages: PUBLIC_BUNDLED_WORKSPACE_PACKAGES,
};
- Step 4: Run public package tests to verify pass
Run:
node --test scripts/build-public-npm-package.test.mjs
Expected: PASS.
- Step 5: Write failing artifact metadata tests
In scripts/package-artifacts.test.mjs, replace expectations that use the
public npm tarball or package version:
assert.equal(layout.cliTarball, '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz');
{
ecosystem: 'npm',
packageName: '@kaelio/ktx',
packageRoot: 'packages/cli',
packageVersion: '0.1.0',
private: false,
releaseMode: 'ci-artifact-only',
}
{
ecosystem: 'npm',
packageName: '@kaelio/ktx',
packageVersion: '0.1.0',
path: 'npm/kaelio-ktx-0.1.0.tgz',
bytes: Buffer.byteLength('@kaelio/ktx-tarball'),
sha256: createHash('sha256').update('@kaelio/ktx-tarball').digest('hex'),
}
In the runtime smoke source expectation, replace:
requireOutput('ktx public package version', version, /@kaelio\/ktx 0\.1\.0/);
In scripts/local-embeddings-runtime-smoke.test.mjs, replace the public
tarball selection assertion with:
assert.equal(
publicKtxTarballName(['kaelio-ktx-0.1.0.tgz', 'ignore-me.tgz']),
'kaelio-ktx-0.1.0.tgz',
);
- Step 6: Run artifact tests to verify failure
Run:
node --test scripts/package-artifacts.test.mjs scripts/local-embeddings-runtime-smoke.test.mjs
Expected: FAIL. The failure mentions stale artifact metadata or tarball
expectations for 0.0.0-private.
- Step 7: Implement artifact metadata versioning
In scripts/package-artifacts.mjs, change the build-public import to:
import {
PUBLIC_NPM_PACKAGE_NAME,
PUBLIC_NPM_PACKAGE_VERSION,
publicNpmPackageTarballName,
} from './build-public-npm-package.mjs';
Replace npmPackageTarballName with:
function npmPackageTarballName(packageName) {
if (packageName === PUBLIC_NPM_PACKAGE_NAME) {
return publicNpmPackageTarballName(PUBLIC_NPM_PACKAGE_VERSION);
}
return `${packageName.replace('@ktx/', 'ktx-')}-${PACKAGE_VERSION}.tgz`;
}
In readNpmPackageMetadata, return the public package version for
@kaelio/ktx:
const isPublicKtxPackage = packageInfo.name === PUBLIC_NPM_PACKAGE_NAME;
return releaseMetadataEntry({
ecosystem: 'npm',
packageName: packageInfo.name,
packageRoot: packageInfo.packageRoot,
packageVersion: isPublicKtxPackage ? PUBLIC_NPM_PACKAGE_VERSION : packageJson.version,
privatePackage: isPublicKtxPackage ? false : packageJson.private === true,
});
In npmRuntimeSmokeSource, replace the version output regex with:
requireOutput('ktx public package version', version, /@kaelio\/ktx 0\.1\.0/);
- Step 8: Run artifact tests to verify pass
Run:
node --test scripts/package-artifacts.test.mjs scripts/local-embeddings-runtime-smoke.test.mjs
Expected: PASS.
- Step 9: Commit public version stamping
Run:
git add scripts/build-public-npm-package.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/local-embeddings-runtime-smoke.test.mjs
git commit -m "build: stamp public npm package version"
Expected: commit created.
Task 2: Add publish-ready release policy validation
Files:
-
Modify:
scripts/release-readiness.mjs -
Modify:
scripts/release-readiness.test.mjs -
Modify:
release-policy.json -
Step 1: Write failing release readiness tests
In scripts/release-readiness.test.mjs, add PUBLIC_NPM_PACKAGE_VERSION to
the imports from ./build-public-npm-package.mjs:
import { PUBLIC_NPM_PACKAGE_VERSION } from './build-public-npm-package.mjs';
Update releasePolicy() so the default npm block includes publish settings:
npm: {
publish: false,
registry: null,
access: 'public',
tag: 'latest',
packages: ['@kaelio/ktx'],
...npmOverrides,
},
In each existing releaseReadinessReport expected object for
ci-artifact-only and published-package-smoke-required, add:
npmPublish: null,
Place it after publishedPackageSmokeGate and before
blockedPublishingDecisions.
In writeReleaseMetadataInputs, keep internal workspace package versions
private. The public package version comes from artifact metadata:
version: '0.0.0-private',
private: true,
Add this test after the existing
reports required published package smoke when release mode requires it test:
it('accepts the npm public release ready policy', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-npm-public-ready-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,
},
requiredBeforePublishing: [],
}),
});
const report = await releaseReadinessReport(root);
assert.deepEqual(report, {
schemaVersion: 1,
releaseMode: 'npm-public-release-ready',
sourceRevision: 'abc123',
npmPublishEnabled: true,
pythonPublishEnabled: false,
packageNames: ['@kaelio/ktx', 'ktx-sl', 'ktx-daemon', 'kaelio-ktx'],
publishedPackageSmokeGate: {
status: 'required',
script: 'pnpm run release:published-smoke',
reason: 'Run the published package smoke after the npm package is published.',
configSource: 'release-policy',
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
registry: null,
},
npmPublish: {
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
access: 'public',
tag: 'latest',
registry: null,
},
blockedPublishingDecisions: [],
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
Add this validation test:
it('rejects npm public release ready mode when npm publish is disabled', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-npm-public-ready-disabled-test-'));
try {
await writeReadyFixture(root, {
policy: releasePolicy({
releaseMode: 'npm-public-release-ready',
npm: {
publish: false,
registry: null,
access: 'public',
tag: 'latest',
},
publishedPackageSmoke: {
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
registry: null,
},
requiredBeforePublishing: [],
}),
});
await assert.rejects(
() => releaseReadinessReport(root),
/npm-public-release-ready policy requires npm.publish true/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
Add this validation test:
it('rejects npm public release ready mode when Python publishing is enabled', async () => {
const root = await mkdtemp(join(tmpdir(), 'ktx-npm-public-ready-python-test-'));
try {
await writeReadyFixture(root, {
policy: releasePolicy({
releaseMode: 'npm-public-release-ready',
npm: {
publish: true,
registry: null,
access: 'public',
tag: 'latest',
},
python: {
publish: true,
repository: 'pypi',
},
publishedPackageSmoke: {
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
registry: null,
},
requiredBeforePublishing: [],
}),
});
await assert.rejects(
() => releaseReadinessReport(root),
/npm-public-release-ready policy keeps python.publish false/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
- Step 2: Run release readiness tests to verify failure
Run:
node --test scripts/release-readiness.test.mjs
Expected: FAIL with Unsupported release policy releaseMode: npm-public-release-ready or missing npm.access validation.
- Step 3: Implement publish-ready policy validation
In scripts/release-readiness.mjs, import the public package version:
import { PUBLIC_NPM_PACKAGE_VERSION } from './build-public-npm-package.mjs';
Add the release mode constant and include it in SUPPORTED_RELEASE_MODES:
const NPM_PUBLIC_RELEASE_READY_MODE = 'npm-public-release-ready';
const SUPPORTED_RELEASE_MODES = new Set([
CI_ARTIFACT_ONLY_RELEASE_MODE,
PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE,
NPM_PUBLIC_RELEASE_READY_MODE,
]);
Add string validators for the npm publish settings:
function assertNpmAccess(value) {
if (value !== 'public') {
throw new Error('Release policy npm.access must be public');
}
}
function assertNpmTag(value) {
assertString(value, 'Release policy npm.tag');
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value)) {
throw new Error(`Invalid Release policy npm.tag: ${value}`);
}
}
In validateReleasePolicy, validate the new npm fields after
assertNullableString(policy.npm.registry, 'Release policy npm.registry');:
assertNpmAccess(policy.npm.access);
assertNpmTag(policy.npm.tag);
Replace assertRequiredBeforePublishing with:
function assertRequiredBeforePublishing(policy) {
assertStringArray(policy.requiredBeforePublishing, 'Release policy requiredBeforePublishing');
if (policy.releaseMode === CI_ARTIFACT_ONLY_RELEASE_MODE && policy.requiredBeforePublishing.length === 0) {
throw new Error('Release policy requiredBeforePublishing must list the remaining publishing decisions');
}
if (
(policy.releaseMode === PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE ||
policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE) &&
policy.requiredBeforePublishing.length > 0
) {
throw new Error(`${policy.releaseMode} release mode requires requiredBeforePublishing to be empty`);
}
}
Replace publishedPackageSmokeGate with:
function publishedPackageSmokeGate(policy) {
const config = readPublishedPackageSmokeConfig({}, [], policy.publishedPackageSmoke);
if (
(policy.releaseMode === PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE ||
policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE) &&
!config.enabled
) {
throw new Error(`${policy.releaseMode} release mode requires release-policy.json publishedPackageSmoke.packageName`);
}
const base =
policy.releaseMode === CI_ARTIFACT_ONLY_RELEASE_MODE
? {
status: 'not_required',
reason: 'Published package smoke remains pending until release-policy.json enables npm registry publishing.',
}
: policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE
? {
status: 'required',
reason: 'Run the published package smoke after the npm package is published.',
}
: {
status: 'required',
reason: 'Run the published package smoke before accepting the hybrid-search release.',
};
return {
...base,
script: 'pnpm run release:published-smoke',
configSource: config.enabled ? config.configSource : null,
packageName: config.enabled ? config.packageName : null,
version: config.enabled ? config.packageVersion : policy.publishedPackageSmoke.version,
registry: config.enabled ? (config.registry ?? null) : policy.publishedPackageSmoke.registry,
};
}
Add this function below assertNonPublishingArtifactPolicy:
function assertNpmPublicReleaseReadyPolicy(policy, metadata) {
if (policy.npm.publish !== true) {
throw new Error('npm-public-release-ready policy requires npm.publish true');
}
if (policy.python.publish !== false) {
throw new Error('npm-public-release-ready policy keeps python.publish false');
}
if (policy.python.repository !== null) {
throw new Error('npm-public-release-ready policy keeps python.repository null');
}
assertSameMembers(policy.npm.packages, ['@kaelio/ktx'], 'Release policy npm.packages');
assertSameMembers(policy.python.packages, metadataNames(metadata, 'python'), 'Release policy python.packages');
const npmMetadata = metadata.find((entry) => entry.ecosystem === 'npm' && entry.packageName === '@kaelio/ktx');
if (!npmMetadata) {
throw new Error('npm-public-release-ready policy requires @kaelio/ktx artifact metadata');
}
if (npmMetadata.private !== false) {
throw new Error('npm-public-release-ready policy requires @kaelio/ktx to be publishable');
}
if (npmMetadata.packageVersion !== PUBLIC_NPM_PACKAGE_VERSION) {
throw new Error(
`npm-public-release-ready policy expected @kaelio/ktx ${PUBLIC_NPM_PACKAGE_VERSION}, got ${npmMetadata.packageVersion}`,
);
}
if (policy.publishedPackageSmoke.packageName !== '@kaelio/ktx') {
throw new Error('npm-public-release-ready policy requires publishedPackageSmoke.packageName @kaelio/ktx');
}
if (policy.publishedPackageSmoke.version !== PUBLIC_NPM_PACKAGE_VERSION) {
throw new Error(
`npm-public-release-ready policy requires publishedPackageSmoke.version ${PUBLIC_NPM_PACKAGE_VERSION}`,
);
}
}
Inside assertNonPublishingArtifactPolicy, replace the npm package version
suffix check with public-package-aware validation:
if (isPublicKtxPackage) {
if (entry.packageVersion !== PUBLIC_NPM_PACKAGE_VERSION) {
throw new Error(
`${policyLabel} npm package @kaelio/ktx must use public version ${PUBLIC_NPM_PACKAGE_VERSION}`,
);
}
} else if (!entry.packageVersion.endsWith('-private')) {
throw new Error(`${policyLabel} npm package ${entry.packageName} must use a private version suffix`);
}
In releaseReadinessReport, replace the unconditional
assertNonPublishingArtifactPolicy(policy, metadata); call with:
if (policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE) {
assertNpmPublicReleaseReadyPolicy(policy, metadata);
} else {
assertNonPublishingArtifactPolicy(policy, metadata);
}
Add npmPublish to the returned report:
npmPublish:
policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE
? {
packageName: '@kaelio/ktx',
version: PUBLIC_NPM_PACKAGE_VERSION,
access: policy.npm.access,
tag: policy.npm.tag,
registry: policy.npm.registry,
}
: null,
Update the text output so it prints the npm publish target when present:
if (report.npmPublish) {
process.stdout.write(
`NPM publish target: ${report.npmPublish.packageName}@${report.npmPublish.version} (${report.npmPublish.tag})\n`,
);
} else {
process.stdout.write('Registry publishing remains disabled by release-policy.json.\n');
}
- Step 4: Update release policy
Replace release-policy.json with:
{
"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
},
"requiredBeforePublishing": []
}
- Step 5: Run release readiness tests to verify pass
Run:
node --test scripts/release-readiness.test.mjs
Expected: PASS.
- Step 6: Commit release policy validation
Run:
git add scripts/release-readiness.mjs scripts/release-readiness.test.mjs release-policy.json
git commit -m "release: add npm public release policy"
Expected: commit created.
Task 3: Add guarded npm publish script
Files:
-
Create:
scripts/publish-public-npm-package.test.mjs -
Create:
scripts/publish-public-npm-package.mjs -
Modify:
package.json -
Step 1: Write failing publish script tests
Create scripts/publish-public-npm-package.test.mjs with:
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { describe, it } from 'node:test';
import {
buildNpmPublishCommand,
requireNpmPublicReleaseReady,
resolvePublishMode,
} from './publish-public-npm-package.mjs';
const readyReport = {
releaseMode: 'npm-public-release-ready',
npmPublishEnabled: true,
npmPublish: {
packageName: '@kaelio/ktx',
version: '0.1.0',
access: 'public',
tag: 'latest',
registry: null,
},
};
describe('resolvePublishMode', () => {
it('dry-runs by default', () => {
assert.deepEqual(resolvePublishMode([]), { live: false });
});
it('requires an explicit flag for live publish', () => {
assert.deepEqual(resolvePublishMode(['--publish']), { live: true });
});
});
describe('requireNpmPublicReleaseReady', () => {
it('accepts the npm public release ready report', () => {
assert.equal(requireNpmPublicReleaseReady(readyReport), readyReport.npmPublish);
});
it('rejects artifact-only reports', () => {
assert.throws(
() =>
requireNpmPublicReleaseReady({
releaseMode: 'ci-artifact-only',
npmPublishEnabled: false,
npmPublish: null,
}),
/release-policy.json must use npm-public-release-ready before publishing/,
);
});
});
describe('buildNpmPublishCommand', () => {
it('builds a dry-run pnpm publish command by default', () => {
assert.deepEqual(buildNpmPublishCommand('/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz', readyReport.npmPublish, { live: false }), {
command: 'pnpm',
args: [
'publish',
'/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz',
'--access',
'public',
'--tag',
'latest',
'--dry-run',
],
env: {},
});
});
it('omits dry-run only for explicit live publish', () => {
assert.deepEqual(buildNpmPublishCommand('/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz', readyReport.npmPublish, { live: true }).args, [
'publish',
'/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz',
'--access',
'public',
'--tag',
'latest',
]);
});
it('uses npm_config_registry when a registry is configured', () => {
const publish = {
...readyReport.npmPublish,
registry: 'https://registry.npmjs.org/',
};
assert.deepEqual(
buildNpmPublishCommand('/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0.tgz', publish, { live: false }).env,
{ npm_config_registry: 'https://registry.npmjs.org/' },
);
});
});
describe('package script', () => {
it('registers release:npm-publish', async () => {
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
assert.equal(packageJson.scripts['release:npm-publish'], 'node scripts/publish-public-npm-package.mjs');
});
});
- Step 2: Run publish script tests to verify failure
Run:
node --test scripts/publish-public-npm-package.test.mjs
Expected: FAIL with Cannot find module for
scripts/publish-public-npm-package.mjs.
- Step 3: Implement publish script
Create scripts/publish-public-npm-package.mjs with:
#!/usr/bin/env node
import { execFile } from 'node:child_process';
import { access } from 'node:fs/promises';
import { promisify } from 'node:util';
import { pathToFileURL } from 'node:url';
import { packageArtifactLayout } from './package-artifacts.mjs';
import { releaseReadinessReport } from './release-readiness.mjs';
const execFileAsync = promisify(execFile);
export function resolvePublishMode(args = process.argv.slice(2)) {
return { live: args.includes('--publish') };
}
export function requireNpmPublicReleaseReady(report) {
if (report.releaseMode !== 'npm-public-release-ready' || report.npmPublishEnabled !== true || !report.npmPublish) {
throw new Error('release-policy.json must use npm-public-release-ready before publishing');
}
return report.npmPublish;
}
export function buildNpmPublishCommand(tarballPath, publish, mode) {
return {
command: 'pnpm',
args: [
'publish',
tarballPath,
'--access',
publish.access,
'--tag',
publish.tag,
...(mode.live ? [] : ['--dry-run']),
],
env: publish.registry ? { npm_config_registry: publish.registry } : {},
};
}
async function assertFileExists(path) {
try {
await access(path);
} catch {
throw new Error(`Missing npm tarball: ${path}. Run pnpm run artifacts:check first.`);
}
}
async function runPublishCommand(command) {
process.stdout.write(`$ ${command.command} ${command.args.join(' ')}\n`);
await execFileAsync(command.command, command.args, {
env: { ...process.env, ...command.env },
encoding: 'utf8',
maxBuffer: 1024 * 1024 * 20,
});
}
export async function publishPublicNpmPackage(options = {}) {
const rootDir = options.rootDir;
const mode = options.mode ?? resolvePublishMode(options.args);
const report = await releaseReadinessReport(rootDir);
const publish = requireNpmPublicReleaseReady(report);
const layout = packageArtifactLayout(rootDir);
const tarballPath = layout.cliTarball;
await assertFileExists(tarballPath);
const command = buildNpmPublishCommand(tarballPath, publish, mode);
await runPublishCommand(command);
process.stdout.write(
mode.live
? `Published ${publish.packageName}@${publish.version} with tag ${publish.tag}\n`
: `Dry-run verified ${publish.packageName}@${publish.version} with tag ${publish.tag}\n`,
);
}
async function main() {
await publishPublicNpmPackage({ args: process.argv.slice(2) });
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
try {
await main();
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
process.exitCode = 1;
}
}
- Step 4: Add the package script
In root package.json, add this script after release:local-embeddings-smoke:
"release:npm-publish": "node scripts/publish-public-npm-package.mjs",
- Step 5: Run publish script tests to verify pass
Run:
node --test scripts/publish-public-npm-package.test.mjs
Expected: PASS.
- Step 6: Run a dry-run publish after artifacts are built
Run:
pnpm run artifacts:check
pnpm run release:npm-publish
Expected: PASS. The publish command includes --dry-run, and the final line is:
Dry-run verified @kaelio/ktx@0.1.0 with tag latest
- Step 7: Commit publish script
Run:
git add scripts/publish-public-npm-package.mjs scripts/publish-public-npm-package.test.mjs package.json
git commit -m "release: add guarded npm publish script"
Expected: commit created.
Task 4: Add manual release workflow and docs
Files:
-
Create:
.github/workflows/release.yml -
Create:
scripts/release-workflow.test.mjs -
Modify:
README.md -
Step 1: Write failing workflow tests
Create scripts/release-workflow.test.mjs with:
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { describe, it } from 'node:test';
describe('release workflow', () => {
it('publishes only from manual dispatch with an explicit live input', async () => {
const workflow = await readFile(new URL('../.github/workflows/release.yml', import.meta.url), 'utf8');
assert.match(workflow, /^name: KTX Release$/m);
assert.match(workflow, /^ workflow_dispatch:$/m);
assert.match(workflow, /publish_live:/);
assert.match(workflow, /default: false/);
assert.match(workflow, /pnpm run artifacts:check/);
assert.match(workflow, /pnpm run release:readiness/);
assert.match(workflow, /pnpm run release:npm-publish$/m);
assert.match(workflow, /pnpm run release:npm-publish -- --publish/);
assert.match(workflow, /NODE_AUTH_TOKEN: \$\{\{ secrets.NPM_TOKEN \}\}/);
assert.doesNotMatch(workflow, /^ push:/m);
assert.doesNotMatch(workflow, /^ pull_request:/m);
});
});
- Step 2: Run workflow tests to verify failure
Run:
node --test scripts/release-workflow.test.mjs
Expected: FAIL because .github/workflows/release.yml does not exist.
- Step 3: Add the release workflow
Create .github/workflows/release.yml with:
name: KTX Release
on:
workflow_dispatch:
inputs:
publish_live:
description: "Publish @kaelio/ktx to npm instead of running a dry-run"
required: true
type: boolean
default: false
permissions:
contents: read
concurrency:
group: ktx-release-${{ github.ref }}
cancel-in-progress: false
jobs:
npm-public-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Install TypeScript dependencies
run: pnpm install --frozen-lockfile
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: Setup uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install Python dependencies
run: uv sync --all-packages
- name: Build and verify artifacts
run: pnpm run artifacts:check
- name: Check release readiness
run: pnpm run release:readiness
- name: Dry-run npm publish
if: ${{ !inputs.publish_live }}
run: pnpm run release:npm-publish
- name: Publish npm package
if: ${{ inputs.publish_live }}
run: pnpm run release:npm-publish -- --publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- Step 4: Update release docs
In README.md, replace the current ## Release status section with:
## Release status
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.
Build local package artifacts and verify the guarded dry-run publish path with:
```bash
source .venv/bin/activate
pnpm run artifacts:check
pnpm run release:readiness
pnpm run release:npm-publish
Run the live npm publish only from the manual KTX Release workflow with the
publish_live input enabled after the NPM_TOKEN secret is configured.
- [ ] **Step 5: Run workflow and README checks**
Run:
```bash
node --test scripts/release-workflow.test.mjs scripts/examples-docs.test.mjs
Expected: PASS.
- Step 6: Commit workflow and docs
Run:
git add .github/workflows/release.yml scripts/release-workflow.test.mjs README.md
git commit -m "release: document public npm release handoff"
Expected: commit created.
Task 5: Final verification
Files:
-
Verify:
scripts/*.test.mjs -
Verify:
packages/cli/src/* -
Verify:
README.md -
Verify:
release-policy.json -
Step 1: Run focused script tests
Run:
node --test scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs scripts/local-embeddings-runtime-smoke.test.mjs scripts/release-readiness.test.mjs scripts/publish-public-npm-package.test.mjs scripts/published-package-smoke.test.mjs scripts/release-workflow.test.mjs scripts/examples-docs.test.mjs
Expected: PASS.
- Step 2: Run workspace type and package checks
Run:
pnpm run type-check
pnpm run artifacts:check
Expected: PASS. The artifact build creates
dist/artifacts/npm/kaelio-ktx-0.1.0.tgz.
- Step 3: Run release readiness and dry-run publish
Run:
pnpm run release:readiness
pnpm run release:npm-publish
Expected: PASS. release:readiness prints KTX release mode: npm-public-release-ready, and release:npm-publish prints Dry-run verified @kaelio/ktx@0.1.0 with tag latest.
- Step 4: Run pre-commit for changed files
Run:
uv run pre-commit run --files scripts/build-public-npm-package.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/local-embeddings-runtime-smoke.test.mjs scripts/release-readiness.mjs scripts/release-readiness.test.mjs scripts/publish-public-npm-package.mjs scripts/publish-public-npm-package.test.mjs scripts/release-workflow.test.mjs release-policy.json package.json README.md .github/workflows/release.yml
Expected: PASS. If pre-commit is unavailable because the local uv version or
pre-commit environment is missing, report that explicitly and keep the script
tests, pnpm run type-check, pnpm run artifacts:check, pnpm run release:readiness, and pnpm run release:npm-publish results as the closest
checks.
- Step 5: Confirm the worktree is clean
Run:
git status --short
Expected: no output. If there are uncommitted tracked changes, inspect them and commit only files from this plan with the exact task commit commands above.
Success criteria
@kaelio/ktxartifact metadata and tarball names use version0.1.0.release-policy.jsonencodesnpm-public-release-ready,npm.publish: true, andpython.publish: false.pnpm run release:npm-publishperforms a dry-run by default.- Live npm publishing requires
pnpm run release:npm-publish -- --publishor the manualKTX Releaseworkflow withpublish_liveenabled. - Published-package smoke remains the post-publication proof for
npx @kaelio/ktx, localnpx ktx, and globalktxinvocation modes. - No Python package publication is added for this release.
Self-review
- Spec coverage: this plan covers the remaining public npm handoff gap while preserving the bundled Python wheel model and single npm package surface.
- Placeholder scan: no open placeholders or deferred implementation notes are present.
- Type consistency: the release mode name is consistently
npm-public-release-ready; the public npm version is consistently0.1.0; the publish script consumes thenpmPublishreport shape produced byrelease-readiness.mjs.