Contributing to the Kindgi SDK
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).
pnpm installpnpm run ci # lint, border check, spec validation, build, typecheck, test, publish checksGitHub 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.
With your coding agent
Section titled “With your coding agent”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-Bytrailer on its commits; - a
## Checkslist 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 bypnpm install) andpnpm 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:borderenforces this. -
HTTP routes. Adding or changing a route in
@kindgi/apifollows 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:validatechecks 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 bypnpm install) refuses such a message before the commit exists, while rewording is still easy. A real term that looks like one goes inALLOWED_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.
Releases
Section titled “Releases”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).
- Every user-visible change adds a changeset:
pnpm changeset. Before 1.0, and while release candidates are out, it's apatch: oneminormoves 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 insdks/pythonorsdks/python-cliadds its line under Unreleased insdks/python/CHANGELOG.md, and one insdks/javaorsdks/scalainsdks/java/CHANGELOG.md, in the same pull request. - Merging to
mainupdates the "Version Packages" pull request (opened by the org's release GitHub App, so CI runs on it like any other pull request). - Merging that pull request bumps versions and changelogs. Run
pnpm run ci:localon its branch first, so the docs tutorials pass against the release. - 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.
Maven Central
Section titled “Maven Central”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):
- 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. scripts/check-jars.mjschecks 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 undercom/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.scripts/central-bundle.mjsbundles 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.- 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.
Release candidates
Section titled “Release candidates”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, neverlatest(scripts/release-dist-tag.mjs), and the Release workflow checks thatlatestis 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'skindgi init --template=javaandkindgi buildneed 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 initpins the rc packages, a Python pack getskindgi>=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:
- Cut the branch from
mainat the commit the release is to be:git push origin <commit>:refs/heads/release/X.Y.Z. - Enter pre mode on it: a pull request into
release/X.Y.Zwithpnpm 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 versionsX.Y.Z-rc.0, thenrc.1, and so on, and moves the changesets it versions into.changeset/pre/. While candidates are out, every changeset is apatch: aminorwould move the release itself. - 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-runoff: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-clijob waits on npm. It builds its wheel from the@kindgi/clijust published, and npm can take minutes to serve a new version tonpm install. Approve that job's environment oncenpm install @kindgi/cli@<version>succeeds in an empty folder.
- 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
(
- A fix for the release goes into
release/X.Y.Zin its own pull request, with its changeset, and the next candidate carries it. When the next release needs it too, land it onmainas well. - The final: a pull request into
release/X.Y.Zwithpnpm 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 versionsX.Y.Z: each changelog'sX.Y.Zsection gathers the candidates' changesets, and.changeset/pre.jsonand.changeset/pre/go. Merge it, dry-run the Release workflow, then publish; it goes out underlatest. - Back to
main: mergerelease/X.Y.Zintomainin a pull request (a merge commit), somainhas 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
mainafter the cut moves to the release's version, since the fixed group has one version (pnpm run check:jvm-versionandcheck:python-versionfail otherwise); - in
sdks/python/CHANGELOG.mdandsdks/java/CHANGELOG.md,main's Unreleased entries stay under Unreleased, above the release's sections.
- delete
- Docs after the release: a fix to the release's pages goes into
its
release-docs/X.Y.Zbranch, cut at the release's tag (site/README.md), and ontomaintoo. A candidate's/next/pages build the same way, fromrelease-docs/X.Y.Z-rc.N. A docs change for this release that merged tomainafter the cut reaches its pages the same way: itssite/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.