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-coreis the onlybundleDependenciesentry, because it is afile: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 aninBundleentry that neitheroverridesnornpm audit fixcan 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 ordinarydependenciesthat hoist into each package's ownnode_modules, which is what lets a packager override one and what putstsc/tsserverwhere an editor looks for them. Container-runtime consumes the builtdist/, 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 toinclude/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 carriespackage-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).
-
Bump
package.json, add the matchingCHANGELOG.mdentry, and runmake sync-templateto move the package template's@start9labs/start-sdkpin to the same version. Land them together onmaster.pre-checkenforces the match, so a stale pin fails the release before anything is tagged or published. Packaging workspaces tracklive-docs, so changingmasterdoes not update their template; the SDK tag advances it. -
Cut the release from the repo root (needs
ghand an npm login with publish rights):./scripts/manage-release.sh release start-sdkThis 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, notmake 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 publishis 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*', ornpm view @start9labs/start-sdk versions); the topCHANGELOG.mdheading is the prospective next SDK version. If it has no matchingstart-sdk/v<version>tag it is unreleased — add your entry under it (### Added/Changed/Fixed/Security/Removed), raising the number andpackage.jsonversiononly for a larger tier (see the next bullet, and the rootAGENTS.mdchangelog 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 bundleverifies nothing. It buildsdist/and stops —testandcheck-fmtare deliberately not prerequisites (consumers like the OS build would otherwise re-run jest every build). Runmake testandmake check-fmtexplicitly. Andcheck-fmtisnpx prettier "**/*.ts" --check— TypeScript only. The CI gate is repo-rootprettier --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.
tschere cannot exercise the SDK's own public API — nothing internal calls it. Use themake link+npm linkflow from a service package and run that package'stsc. 2.0.8 shippedsdk.host.getBridgeAddressmissing itsfallbackPortoverload 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, runmake bundlebefore 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'slib/. A new start-core export must be re-exported fromlib/index.tsor exposed viaStartSdk.build(). - OS bindings (
shared-libs/ts-modules/start-core/lib/osBindings/) mirror Rust types inshared-libs/crates/start-core; regenerate/update them when the Rust side changes. - Editing
s9pk.mk/tsconfig.base.jsonchanges 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/'spackage.jsondeclares the@start9labs/start-sdkversion scaffolded packages build against;make sync-templatemoves that pin to the current version andpre-checkholds it at the release being cut. It deliberately commits nopackage-lock.json—s9pk init-packagerunsnpm 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-checkrejects a re-committed template lockfile. - Prettier config (single quotes, no semis, trailing commas, 2-space,
arrowParens: avoid) lives in each sub-package'spackage.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.mdships to every packager.start-cli s9pk init-workspacesymlinks it in as each workspace'sAGENTS.md(theAGENTS_SYMLINK_TARGETconst inshared-libs/crates/start-core/src/s9pk/init.rs). A workspace trackslive-docs, so an edit reaches packagers on their next sync once it is on that branch — immediately for a fix PR'd tolive-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-packagecopies it verbatim, interpolating{{id}}and{{name}}(escaped for TypeScript string literals in.tsfiles) and skippingnode_modules/,.git/, andjavascript/. Keep it buildable; a broken template breaks every new package. Its.github/workflows/are a hand-maintained mirror — see the repo-rootAGENTS.md§ Coupled changes.- Recipes name constructs; reference pages teach them. Code examples belong on reference pages. A new
recipe-*.mdneeds an entry in the intent table indocs/src/recipes.md, not just inSUMMARY.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.