AGENTS.md — start-sdk

The TypeScript SDK (@start9labs/start-sdk) for building StartOS service packages. Lives at projects/start-sdk/ inside the start-technologies monorepo. One npm package plus the packaging build wrapper and the packaging mdbook. CLAUDE.md is a one-line @AGENTS.md import.

This file is both the contribution guide and the agent/dev operating rules for this scope — how to set up, build, test, and release the SDK, plus the gotchas. It has no CONTRIBUTING.md: this scope's was folded in here. See ARCHITECTURE.md for how the SDK is structured — modules, data flow, the build pipeline, and the design patterns (builder chains, effects-as-capabilities, manifest type threading). If you are building a service package using the SDK rather than developing the SDK itself, you want the packaging docs.

Read up the tree first. These docs are hierarchical: before working here, read the AGENTS.md in each enclosing directory up to the repo root (and their ARCHITECTURE.md / CONTRIBUTING.md where relevant). This file covers only what is specific to this scope and does not repeat rules already stated higher up — commit/PR conventions live in the root CONTRIBUTING.md.

Prerequisites

Node.js v22+ (nvm recommended), npm, and GNU Make.

Layout

  • lib/@start9labs/start-sdk: developer-facing facade (StartSdk), daemons, health checks, backups, file helpers, subcontainers, i18n, triggers. Imports core types, OS bindings, ABI, Effects, ExVer parser, actions/input builders, interfaces, dependencies, s9pk reader from @start9labs/start-core (shared-libs/ts-modules/start-core/).
  • dist/ — build output (generated; what publishes to npm). @start9labs/start-core is the only bundleDependencies entry, because it is a file: dependency that is never published and so nothing downstream could resolve it. Keep it that way: anything else added to that array reaches consuming packages as an inBundle entry that neither overrides nor npm audit fix can touch, which makes an SDK release the only way to clear an advisory against it. The package toolchain — typescript, prettier, eslint, typescript-eslint, @vercel/ncc, @types/node — ships as ordinary dependencies that hoist into each package's own node_modules, which is what lets a packager override one and what puts tsc/tsserver where an editor looks for them. Container-runtime consumes the built dist/, not the source.
  • Makefile — build orchestration for the SDK itself.
  • s9pk.mk, tsconfig.base.json — build plumbing shipped inside the published package for service packages to include/extends. Marked DO NOT EDIT in the consuming-package contract; edits here change the contract for every package.
  • docs/ — the "Service Packaging" mdbook (book.toml), published at docs.start9.com/packaging. Also carries package-template/ and the workspace agent context. See Docs.
  • CHANGELOG.md — Keep a Changelog style, headings ## <sdk-version> — StartOS <os-version> (<date>).

Build & test (run from projects/start-sdk/)

Command What
make node_modules npm ci
make bundle build only: build @start9labs/start-core (prerequisite), compile SDK → dist/. Deliberately does not run test or check-fmt — see Gotchas
make test jest
make check tsc --noEmit
make fmt / make check-fmt Prettier write / check on all .ts
make link build + npm link from dist/ for local package testing
make clean remove dist/, node_modules, generated test output
make publish the raw npm step only — not how you cut a release (see Gotchas)

Tests are jest + ts-jest, Node only (no browser). Test files use .test.ts and are excluded from compilation via tsconfig.json. Run one with npx jest --testPathPattern=host. The bundled @start9labs/start-core has its own suite and build: cd ../../shared-libs/ts-modules/start-core && make test (or make dist). The ExVer parser is generated from that lib's lib/exver/exver.pegjs via Peggy (make runs this for you).

Both packages are strict TypeScript, ES2021 target, CommonJS output. ARCHITECTURE.md covers what the build actually does.

Testing SDK changes against a service package

No publish needed — build and link:

make link                        # from projects/start-sdk/
npm link @start9labs/start-sdk   # from your service package

This symlinks the built dist/ into your global node_modules, so the package picks up local SDK changes.

Cutting a release

The SDK is a first-class project of the monorepo-wide release tool, scripts/manage-release.sh (the npm kind). The version is read from package.json; the git tag / GitHub release is start-sdk/v<version>. Only dist/ ships to npm (compiled JavaScript, declarations, bundled dependencies, package metadata).

  1. Bump package.json, add the matching CHANGELOG.md entry, and run make sync-template to move the package template's @start9labs/start-sdk pin to the same version. Land them together on master. pre-check enforces the match, so a stale pin fails the release before anything is tagged or published. Packaging workspaces track live-docs, so changing master does not update their template; the SDK tag advances it.

  2. Cut the release from the repo root (needs gh and an npm login with publish rights):

    ./scripts/manage-release.sh release start-sdk
    

    This runs pre-check → tag → create the GitHub release → npm publish. It prompts for your npm 2FA one-time password at publish time; OTP=… skips the prompt.

The step order is deliberate: everything idempotent runs before the one irreversible step. See the Gotcha below for why that matters, and never publish with make publish.

Backfilling a release

If a version reached npm without a tag and release, cut them after the fact with the individual subcommands. The commit to tag is the one the published tarball was built from, which npm records:

npm view @start9labs/start-sdk@<version> gitHead

VERSION=<version> COMMIT=<sha> ./scripts/manage-release.sh tag start-sdk
VERSION=<version> ./scripts/manage-release.sh create-gh-release start-sdk

If that commit never landed on master (e.g. the publish was cut from an unmerged branch), merge it first and tag the resulting master commit instead — verify its shipped subtree matches the published tarball rather than assuming it does.

Gotchas

  • Releasing is ./scripts/manage-release.sh release start-sdk, not make publish. The pipeline is pre-check → tag → GitHub release → npm publish, in that order because npm publish is the one step that can never be redone. make publish is only that last step: run it on its own and the version lands on npm with no git tag and no GitHub release, and the normal flow can't recover (pre-check then refuses the version, and npm won't republish it). 2.0.4 and 2.0.5 shipped this way and had to be backfilled. See Cutting a release above, which also documents the backfill.
  • Bumping the version requires a CHANGELOG entry. Freshly check what's shipped first (git ls-remote --tags origin 'start-sdk/v*', or npm view @start9labs/start-sdk versions); the top CHANGELOG.md heading is the prospective next SDK version. If it has no matching start-sdk/v<version> tag it is unreleased — add your entry under it (### Added/Changed/Fixed/Security/Removed), raising the number and package.json version only for a larger tier (see the next bullet, and the root AGENTS.md changelog rule). Reviews reject version bumps without a changelog entry.
  • Don't cut a new version if the current latest hasn't published to npm. Edit the unpublished version in place, raising it to the tier the accumulated changes warrant.
  • make bundle verifies nothing. It builds dist/ and stops — test and check-fmt are deliberately not prerequisites (consumers like the OS build would otherwise re-run jest every build). Run make test and make check-fmt explicitly. And check-fmt is npx prettier "**/*.ts" --check — TypeScript only. The CI gate is repo-root prettier --check . across every file type, so a CHANGELOG or docs edit that passes here still breaks master.
  • Typecheck a consumer before publishing, not just the SDK. tsc here cannot exercise the SDK's own public API — nothing internal calls it. Use the make link + npm link flow from a service package and run that package's tsc. 2.0.8 shipped sdk.host.getBridgeAddress missing its fallbackPort overload this way: the SDK compiled clean, and twelve packages then couldn't.
  • Consumers read the built output. After editing the SDK or @start9labs/start-core, run make bundle before checking container-runtime.
  • SDK vs start-core: types/ABI/OS-bindings/low-level → @start9labs/start-core (shared-libs/ts-modules/start-core/); developer-facing wrappers/runtime helpers → the SDK's lib/. A new start-core export must be re-exported from lib/index.ts or exposed via StartSdk.build().
  • OS bindings (shared-libs/ts-modules/start-core/lib/osBindings/) mirror Rust types in shared-libs/crates/start-core; regenerate/update them when the Rust side changes.
  • Editing s9pk.mk / tsconfig.base.json changes every package's build — they ship in the published package. Treat as a public contract.
  • The package template pins the SDK and ships no lockfile. docs/package-template/'s package.json declares the @start9labs/start-sdk version scaffolded packages build against; make sync-template moves that pin to the current version and pre-check holds it at the release being cut. It deliberately commits no package-lock.jsons9pk init-package runs npm install, so each scaffold generates its own lock (which the packager then commits); a committed template lock is a generated artifact that only rots out of sync with the pin. pre-check rejects a re-committed template lockfile.
  • Prettier config (single quotes, no semis, trailing commas, 2-space, arrowParens: avoid) lives in each sub-package's package.json.

Docs

README.md (overview + quickstart), ARCHITECTURE.md (modules + data flow), CHANGELOG.md, and this file (contribute + operate). The packaging mdbook in docs/ is the developer-facing reference — update it when you change the SDK's developer surface. Keep all of these current in the same change that alters structure, conventions, build, or surface.

The docs/ mdbook

Authoring conventions shared by every book in the monorepo — mdBook/mdbook-tabs versions, admonitions, tabs, SUMMARY.md, the shared theme/ symlink, cross-book links — live in projects/start-docs/AGENTS.md and its CONTRIBUTING.md. Read those before editing pages. What is specific to this book:

  • docs/src/agent-context.md ships to every packager. start-cli s9pk init-workspace symlinks it in as each workspace's AGENTS.md (the AGENTS_SYMLINK_TARGET const in shared-libs/crates/start-core/src/s9pk/init.rs). A workspace tracks live-docs, so an edit reaches packagers on their next sync once it is on that branch — immediately for a fix PR'd to live-docs, at the next SDK release for one that lands on master. It is an always-on context file first and a book page second: keep it a lean map that points at pages, never a place to inline detail. Moving or renaming it breaks every existing workspace symlink — update the const in the same change.
  • docs/package-template/ is live code, not an illustration. s9pk init-package copies it verbatim, interpolating {{id}} and {{name}} (escaped for TypeScript string literals in .ts files) and skipping node_modules/, .git/, and javascript/. Keep it buildable; a broken template breaks every new package. Its .github/workflows/ are a hand-maintained mirror — see the repo-root AGENTS.md § Coupled changes.
  • Recipes name constructs; reference pages teach them. Code examples belong on reference pages. A new recipe-*.md needs an entry in the intent table in docs/src/recipes.md, not just in SUMMARY.md.
  • Verify SDK claims against lib/, not against the prose. This guide has shipped confidently-worded semantics that were wrong. Before documenting what a call does, read it.