Development
Setup
Requires Node.js 24+ (Krypton LTS) — the engines field enforces the floor, and the esbuild bundles target node24.
# Install + build agent
npm install && npm run build:agent
# Run tests in watch mode
npm run test:watch
# Start API server for manual testing
npm run start
# Interactive shell with a neuron
nsys-cortex create dev-neuron
nsys-cortex shell dev-neuron
The CLI is available as nsys-cortex after linking (npm link -w packages/cortex-cli), or via npx nsys-cortex.
The nsys-cortex launcher runs the compiled single-file bundle (packages/cortex-cli/dist/index.js, built by npm run build via esbuild) on plain node — no per-invocation TypeScript transform, and no dependency on npx/tsx resolution at runtime. If the bundle hasn't been built yet (fresh clone + npm link), the launcher falls back to running the source through tsx. After changing CLI source, rebuild with npm run build -w packages/cortex-cli to refresh the bundle the launcher executes — or delete packages/cortex-cli/dist/ to fall back to source. A smoke test (packages/cortex-cli/src/__tests__/cli-bundle.test.ts) exercises the built artifact on plain node — --version, --help, and the dev-mode guard — building the bundle on demand when missing.
When modifying the agent (packages/cortex-agent/), always rebuild with npm run build:agent — the dev sandbox spawns the compiled bundle, not the TypeScript source. Docker mode runs the bundle baked into the neuron image, so after an agent change also run npm run build:image (the script rebuilds the bundle first; node-pty compiles inside the image, so no host toolchain is needed) — otherwise NSYS_CORTEX_MODE=docker keeps running the previous agent.
Building the package
npm run build:package (after npm run build) assembles the publishable @nsys-cortex/cli in packages/cortex-cli/pkg/ (git-ignored): the CLI bundle as index.js (its first line is #!/usr/bin/env node, so npm's bin shims run it on every OS, Windows included), the agent bundle it spawns in dev mode at cortex-agent/dist/index.js (the path keeps the marker the orphan sweep recognises; findAgentBundle() looks beside the CLI bundle when the repository's packages/cortex-agent/dist/index.js is not under the current directory, and honours NSYS_CORTEX_AGENT_BUNDLE), README.md, LICENSE, NOTICE, and a generated manifest — scripts/build-cli-package.mjs writes it from the root version and the native ranges, with publishConfig.registry pinned to this project's GitLab package registry (https://gitlab.nsysware.com/api/v4/projects/37/packages/npm/), so a bare npm publish from pkg/ cannot reach npmjs. The workspace manifests stay private: true; the generated one is the only manifest ever published. node-pty and @modelcontextprotocol/sdk are optionalDependencies: an install without a C toolchain succeeds, and the MCP SDK is loaded only when an MCP server is configured. --omit=optional leaves both out of a local install (the consumer shape of verify-dev-install and of the package test); npm 11 ignores it — and --no-optional and npm_config_omit — on a global install, so npm install -g always fetches them (--ignore-scripts keeps that toolchain-free; verified 2026-10-30 on npm 11.19.1).
npm run build && npm run build:package
cd packages/cortex-cli/pkg
npm pack --dry-run # LICENSE NOTICE README.md cortex-agent/dist/index.js index.js package.json
npm publish --dry-run # "Publishing to https://gitlab.nsysware.com/api/v4/projects/37/packages/npm/ … (dry-run)"
npm pack && npm install -g ./nsys-cortex-cli-<version>.tgz # any OS with Node 24, no repository checkout
Nothing publishes from a developer machine. The dev channel is a CI job's act: on every master merge the publish-dev job (stage publish, after test) assembles the package, sets its version to <version>-dev.<yyyymmdd>.<short sha> in pkg/ only, writes a .npmrc with the ${CI_JOB_TOKEN} placeholder (expanded by npm at run time — no token in any file or log) and runs npm publish --tag dev (npm 11 refuses a prerelease under latest; the dev channel is the dev dist-tag), which the manifest's pin sends to this project's registry; verify-dev-install then installs that exact version in a clean directory with the same project-endpoint .npmrc and checks the installed manifest's version — the far end is the proof, because a republished version is a silent no-op on this registry (npm publish exits 0 and serves the old bytes), which the unique prerelease version also rules out. Neither job runs on a merge-request pipeline. A consumer installs a dev version with the same two .npmrc lines (a deploy token with read_package_registry outside CI, KB registry guide §3/§5) and npm install @nsys-cortex/cli@<dev version>. npmjs is the release pipeline's manual gate on a tag. The bundle smoke test (cli-bundle.test.ts) packs the package, installs the tarball into a temporary prefix and runs a dev-mode exec from an unrelated directory on every test run.
Continuous Integration
The repo ships a GitLab CI pipeline at .gitlab-ci.yml that runs on every push and merge request. Four stages:
| Stage | What it does | Time |
|---|---|---|
install |
npm ci --prefer-offline; node_modules/ cached by package-lock.json hash + handed off via artifacts |
~30s cold, ~5s warm |
check (typecheck) |
npm run build — tsc -b per workspace (build mode IS a type-check AND emits declarations), plus the CLI's esbuild bundle. dist/ is passed to build as artifacts to skip recompiles |
~15s |
build |
npm run build:agent (esbuild single-file bundle with node-pty external) |
~5s |
test |
vitest run --coverage with verbose + JUnit reporters; JUnit XML → MR "Test Reports" tab; Cobertura → MR diff annotations; Lines : NN% → coverage badge; fails below the coverage floor in vitest.config.ts (see testing.md#coverage) |
~10s |
We deliberately do NOT use tsc --noEmit -p packages/X per workspace in the typecheck stage. With TypeScript project references (which this monorepo uses), --noEmit on a downstream workspace fails with TS6305: Output file '../cortex-shared/dist/index.d.ts' has not been built from source file — the declaration files referenced by the downstream tsconfig were never emitted. tsc -b (build mode), which is what npm run build runs, handles project-reference order correctly and produces the declarations as a side effect of type-checking.
The pipeline uses the node:24-bookworm image — Node 24 (Krypton) is the active LTS as of June 2026. The image is pinned to the major rather than node:lts so a future LTS rotation doesn't silently change the runtime.
Test gating in CI
Two test categories skip themselves automatically when their gate env vars aren't set, so CI doesn't need any secrets configured to get a green pipeline:
- Firecracker integration tests — need
NSYS_CORTEX_TEST_FIRECRACKER=1+ Linux + KVM. Always skipped on the standard runner image. To bring up a host that can run them, see Installing Firecracker on Ubuntu 26.04 LTS. - Docker integration test (
docker-sandbox.test.ts) — needsNSYS_CORTEX_TEST_DOCKER=1, a Docker daemon, and the neuron image (npm run build:image). Skipped by default; wiring it into CI needs a docker-capable runner (Docker-in-Docker service or a shell runner with the daemon socket) — not done yet. - LLM integration tests (
agent-run.test.ts,streaming.test.ts) — needNSYS_CORTEX_INTEGRATION_LLM=1+ANTHROPIC_API_KEY. Always skipped unless you wire those vars as CI secrets (which costs real money per pipeline).
To run them locally, see Testing → LLM-gated integration tests.
Adjusting for your runner setup
The pipeline declares tags: [docker] at the default level so it picks up runners with that tag. If your project uses shared runners or a different tag, edit the tags block at the top of .gitlab-ci.yml.
Type-check policy
The check stage is a real gate — pre-existing type errors in test files were fixed in 0.2.0 so tsc --noEmit now succeeds across every workspace. Don't relax this back to allow_failure: true; treat type errors as build breakage.