Skip to content

Contributing to the Kindgi SDK

View as Markdown

Source in the repository.

Status: the SDK is in preview and we are not accepting external contributions yet. Issues and security reports are welcome (see SECURITY.md for vulnerabilities). This document describes how the repository works for maintainers.

Requires Node.js 22.12+ and pnpm (the version pinned in package.json's packageManager field; corepack enable picks it up).

Terminal window
pnpm install
pnpm run ci # lint, border check, spec validation, build, typecheck, test, publish checks

GitHub runs CI on every pull request that isn't a draft: lint and specs, the build, typecheck, tests and publish checks, the docs site and its samples, and the Python SDK on 3.11 and 3.13. Merging requires them.

One step runs only on your machine: the docs tutorials, which start kindgi dev with the private runtime image (pnpm run docs:tutorials). pnpm run ci:local runs all of CI, the tutorials included, on the clean, pushed commit. It needs pnpm, uv and Docker.

Changes here are made by coding agents and supervised by people: the agent does the work, and you read it before it's submitted. .claude/skills/kindgi-contributing/SKILL.md tells your agent how. Claude Code loads it by itself in a clone, and AGENTS.md points other agents to it. It covers:

  • where a docs page's source is;
  • the checks to run for what changed;
  • the pull request's format.

Every pull request carries:

  • the agent's Co-Authored-By trailer on its commits;
  • a ## Checks list of the checks it ran, with their results;
  • a Supervised-by: line with your own name and email.

A check on the pull request asks for any of them that's missing.

  • License headers. Every source file starts with:

    Apache-2.0
    // Copyright (C) 2026 Kindgi Inc.

    The pre-commit hook (scripts/hooks/pre-commit, installed by pnpm install) and pnpm run check:headers (CI) enforce this.

  • Self-contained packages. Packages depend only on other packages in this repository and on third-party libraries. Runtime capabilities are expressed as binding interfaces that the host supplies — never as a dependency on a runtime implementation. pnpm run check:border enforces this.

  • HTTP routes. Adding or changing a route in @kindgi/api follows docs/ADDING-A-ROUTE.md.

  • Specs. @kindgi/specs (packages/specs/schemas/) holds the canonical wire contracts. Change a schema there first, then every package that bundles a copy (drift tests enforce equality); pnpm run spec:validate checks the whole set.

  • Public text. The repository's files, and a pull request's title, description and commit messages (the squash commit's message is the title and description), leave out internal process notes: development- phase ids, scratch paths, unresolved placeholders. They also leave out a few names the project doesn't use in public, the runtime's internal name among them: describe things by their role ("the runtime", "the runtime's release"). pnpm run check:refs (CI) checks files; the PR text check checks a pull request's own text, and runs again when you edit the description. A pull request's text also leaves out internal tracking references (ticket and step ids, process rule numbers, release batch names, working session names), and its commits are public too: the commit-msg hook (scripts/hooks/commit-msg, installed by pnpm install) refuses such a message before the commit exists, while rewording is still easy. A real term that looks like one goes in ALLOWED_TERMS (scripts/text-scan.mjs), with why. Both say where a name is, never which; the list is kept hashed (scripts/forbidden-names.json, generated outside this repository). The pre-push hook (scripts/hooks/pre-push) also runs a local check when the clone names one (git config kindgi.prePushCheck <program>), and does nothing otherwise.

All @kindgi/* packages share one version (Changesets fixed group), and the Python packages (kindgi on PyPI, sdks/python, and kindgi-cli, sdks/python-cli) ship with that same version. Nobody sets the Python version by hand: pnpm run version-packages runs scripts/sync-python-version.mjs --release after changeset version, which writes the new version to each pyproject.toml, re-locks each uv.lock (it needs uv), and turns sdks/python/CHANGELOG.md's Unreleased heading into the version, as PyPI spells it. A branch whose Python projects lag main runs the script without --release: that moves the versions and stamps nothing. CI fails when they differ (pnpm run check:python-version), and so do both publish jobs. A prerelease is -alpha.N, -beta.N or -rc.N (aN, bN, rcN on PyPI); the script refuses any other.

The JVM SDKs (sdks/java) ship with that version too, spelled as npm spells it (Maven orders it the same way; VersionOrderTest in sdks/java/codegen holds it to that). pnpm run version-packages runs scripts/sync-jvm-version.mjs --release, which sets the root pom's own version and each module's parent version (no other <version>), the Scala module's version.sbt once it exists, and turns sdks/java/CHANGELOG.md's Unreleased heading into the version. A branch whose poms lag main runs the script without --release: that moves the versions and stamps nothing. CI fails when they differ (pnpm run check:jvm-version).

  1. Every user-visible change adds a changeset: pnpm changeset. Before 1.0, and while release candidates are out, it's a patch: one minor moves every package to the next minor, which no installed project reaches. A changeset that means to move the release says so in its body: Release-decision: <the version, who decided, and when>, naming a role (the maintainers), never a person, since the body becomes the changelog entry (pnpm run check:changeset-bumps, in CI). Changesets don't reach the Python packages or the JVM SDKs: a user-visible change in sdks/python or sdks/python-cli adds its line under Unreleased in sdks/python/CHANGELOG.md, and one in sdks/java or sdks/scala in sdks/java/CHANGELOG.md, in the same pull request.
  2. Merging to main updates the "Version Packages" pull request (opened by the org's release GitHub App, so CI runs on it like any other pull request).
  3. Merging that pull request bumps versions and changelogs. Run pnpm run ci:local on its branch first, so the docs tutorials pass against the release.
  4. Publishing is a manual, approved run of the Release workflow (npm trusted publishing with provenance, and PyPI trusted publishing with attestations — no tokens). The JVM SDKs go to Maven Central first, and npm waits for them: the CLI scaffolds Java and Scala packs on kindgi-pack at its own version.

Central takes signed artifacts through the Central Portal, and a version it has is permanent: it can't be changed or deleted. The Release workflow's publish-maven job, in the maven-publish environment (required reviewer):

  1. Maven (./mvnw -P release deploy) and sbt (sbt +publishSigned) build, test, sign and stage the artifacts on disk: every jar with its sources and javadoc jars, every file with its .asc.
  2. scripts/check-jars.mjs checks those files: the artifacts we publish and no others, their version, the pom metadata Central requires, each pom's runtime dependencies against its list, classes only under com/kindgi/ and no test code, the LICENSE and NOTICE, and no forbidden name in any class, source or page. The Java and Scala workflows run it on every pull request, on an unsigned staging.
  3. scripts/central-bundle.mjs bundles the same files, verifying every signature against the release key's fingerprint, and uploads them as one deployment, so a version's Java and Scala artifacts publish together or not at all. A dry run has the Portal validate the deployment and then drops it; nothing is published. A version Central already has is skipped.
  4. Before npm publishes, its job waits until Central serves every JVM artifact at the CLI's version (central-bundle.mjs on-central), so npm never gets a CLI whose kindgi-pack doesn't resolve. A release that skipped Maven stops there.

The environment holds the Portal user token (MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD), the signing key (MAVEN_GPG_PRIVATE_KEY, MAVEN_GPG_PASSPHRASE) and the key's public fingerprint (the variable MAVEN_GPG_FINGERPRINT). The key is imported into a keyring of the job's own and deleted at its end. A new module is published only once it's listed in check-jars.mjs's ARTIFACTS, with its runtime dependencies.

To stage and check locally (a JDK 17 and sbt): ./mvnw -P release -Dgpg.skip=true deploy in sdks/java, sbt +publish in sdks/scala, then node scripts/check-jars.mjs sdks/java/target/central-staging sdks/scala/target/sona-staging.

A release can go out first as release candidates, X.Y.Z-rc.N, that no one gets without asking for them by version:

  • npm: they publish under the dist-tag next, never latest (scripts/release-dist-tag.mjs), and the Release workflow checks that latest is still a release afterwards.
  • PyPI: they're X.Y.ZrcN, which pip and uv install only when asked for by version.
  • Maven Central: they're X.Y.Z-rc.N, published like a release (Central has no tags). A build gets one only by naming it, or through a version range: Maven doesn't keep pre-releases out of ranges. They stay on Central: an rc CLI's kindgi init --template=java and kindgi build need its kindgi-pack there.
  • The public docs describe releases. A candidate's pages are under /next/: they say so, link to the latest release, and aren't indexed (site/README.md).
  • An rc CLI keeps its set together: kindgi init pins the rc packages, a Python pack gets kindgi>=X.Y.ZrcN,<…, and its hints name @kindgi/cli@X.Y.Z-rc.N.

A release's candidates and its final come from its own branch, release/X.Y.Z, so main never freezes: the next release's work keeps merging there. A push to release/** runs the same Changesets step as a push to main, so the branch gets its own "Version Packages" pull request, into the branch.

The steps:

  1. Cut the branch from main at the commit the release is to be: git push origin <commit>:refs/heads/release/X.Y.Z.
  2. Enter pre mode on it: a pull request into release/X.Y.Z with pnpm changeset pre enter rc (.changeset/pre.json), the CLI's runtime pin at the runtime's candidate, by digest (packages/cli/src/dev/runtime-image.ts), and a changeset for it. From then on the branch's Version Packages pull request versions X.Y.Z-rc.0, then rc.1, and so on, and moves the changesets it versions into .changeset/pre/. While candidates are out, every changeset is a patch: a minor would move the release itself.
  3. Publish a candidate: merge its Version Packages pull request, then run the Release workflow on the branch, first as a dry run (the default), then with dry-run off: gh workflow run release.yml --ref release/X.Y.Z -f dry-run=false.
    • Every public package must already be on npm. Trusted publishing can't create one, so a new package needs a maintainer to publish its first, placeholder version and attach the trusted publisher (npm trust) before the run.
    • The kindgi-cli job waits on npm. It builds its wheel from the @kindgi/cli just published, and npm can take minutes to serve a new version to npm install. Approve that job's environment once npm install @kindgi/cli@<version> succeeds in an empty folder.
  4. A fix for the release goes into release/X.Y.Z in its own pull request, with its changeset, and the next candidate carries it. When the next release needs it too, land it on main as well.
  5. The final: a pull request into release/X.Y.Z with pnpm changeset pre exit, the CLI's pin at the runtime's release, by digest, and a changeset for it. The next Version Packages pull request versions X.Y.Z: each changelog's X.Y.Z section gathers the candidates' changesets, and .changeset/pre.json and .changeset/pre/ go. Merge it, dry-run the Release workflow, then publish; it goes out under latest.
  6. Back to main: merge release/X.Y.Z into main in a pull request (a merge commit), so main has the release's versions, changelogs and consumed changesets. In it:
    • delete main's copies of changesets the release released (a fix landed on both carries the same file): left in, the next Version Packages pull request lists them again;
    • a package added on main after the cut moves to the release's version, since the fixed group has one version (pnpm run check:jvm-version and check:python-version fail otherwise);
    • in sdks/python/CHANGELOG.md and sdks/java/CHANGELOG.md, main's Unreleased entries stay under Unreleased, above the release's sections.
  7. Docs after the release: a fix to the release's pages goes into its release-docs/X.Y.Z branch, cut at the release's tag (site/README.md), and onto main too. A candidate's /next/ pages build the same way, from release-docs/X.Y.Z-rc.N. A docs change for this release that merged to main after the cut reaches its pages the same way: its site/ part only.

The runtime image follows the same versions: the runtime's release takes X.Y.Z-rc.N and X.Y.Z from release/X.Y.Z too, and never moves preview for a candidate.