clawboo CLI in apps/cli. Every @clawboo/* library in the workspace is private: true and never reaches npm on its own; the libraries it needs are inlined into the CLI’s shipped bundle. The release machinery is Changesets for versioning + changelog, a CI gate that mirrors the publish steps, and a two-phase publish.yml (open a Version PR, then publish on its merge).
This page is for people working on Clawboo who need to cut a release or understand why the publish posture looks the way it does. It covers the .changeset workflow, the CI gate (lint / typecheck / test / build / verify:catalog / smoke-test-bundle / e2e), the publish.yml flow including its provenance attestation, and the single-artifact posture enforced by a test. For the build mechanics behind it, Turbo, the bundle, assemble-cli.sh, read Monorepo and build.
What ships, and what doesn’t
There are 30 scoped@clawboo/* packages under packages/ (25 top-level plus 5 runtime adapters under packages/adapters/*) plus two apps. Run git grep '"private"' packages and you find "private": true on every library; the same is true of apps/web (@clawboo/web) and docs (@clawboo/docs). The only non-private package in the entire workspace is apps/cli, whose name is clawboo (no scope) and whose files array is just ["dist"]. So pnpm changeset publish skips everything private and publishes exactly one tarball: the CLI.
The libraries still ship; they just travel inside the CLI bundle, not as separate npm installs. The web server’s tsup config marks the whole @clawboo/* scope noExternal, so dist/server.js inlines db, mcp, governance, the adapters, and the rest into one file. assemble-cli.sh copies that server.js, the Vite ui/, and the four bundled MCP stdio bins into apps/cli/dist/. The published tarball is therefore: the CLI entrypoint, the inlined server bundle, the SPA assets, and the MCP bins, nothing else.
This single-artifact posture is enforced by a test, not just a convention.
packagePosture.test.ts walks the library and app directories (packages/*, packages/adapters/*, apps/*) and asserts two invariants: (1) the set of non-private packages is exactly ['clawboo (apps/cli)'], and (2) no non-private package has a runtime @clawboo/* dependency on a private one. The second guard matters because a public package that depended on a private one would publish a manifest pointing at packages that are never published; npm install would 404. The test fails the build if you mark a second package non-private.Version posture
The CLI isclawboo@0.3.1 in apps/cli/package.json and CHANGELOG.md, and v0.3.1 is published: the npm latest dist-tag is clawboo@0.3.1 and the clawboo@0.3.1 git tag exists, so npx clawboo installs 0.3.1.
The CLI’s runtime version string comes from the build, not from reading package.json at runtime: apps/cli/tsup.config.ts injects define: { __CLI_VERSION__: JSON.stringify(pkg.version) }, and src/index.ts reads __CLI_VERSION__ into VERSION (falling back to '0.0.0-dev' when the define is absent, e.g. running the TS directly in dev). So clawboo --version reports whatever version was set in package.json at build time.
Changesets
Versioning and changelog generation are driven by Changesets. The configuration in.changeset/config.json is deliberately small:
ignore: ["@clawboo/docs", "@clawboo/web"]keeps the docs placeholder and the web app out of the changeset version-bumping flow entirely. (They are private anyway, but ignoring them stops Changesets from prompting about them or bumping them.) The CLI (clawboo) is the package Changesets actually versions.commit: false: Changesets doesn’t commit on your behalf; the CI action does that step explicitly (it commits the version bump aschore: version packages).access: "public"is the npm publish access for the artifact it does publish (the CLI). It’s harmless for the private packages because they’re never published.updateInternalDependencies: "patch"governs how an internalworkspace:*dependency edge gets re-versioned when a dependency bumps, relevant only if a private package were ever published, which it isn’t, so it has no practical effect today.
Authoring a changeset
The release flow is intent-first: you describe the change in a.changeset/*.md file, and the bump + changelog are derived from it later.
clawboo) and at what semver level (patch / minor / major), then writes a markdown file under .changeset/ describing the bump. You commit that .md file alongside your code change. A clone of main may or may not carry pending changesets: ls .changeset tells you which phase the next publish.yml run will take, because any *.md present means the next run opens a Version PR, and none present means it publishes.
Because the libraries don’t publish, a changeset is in practice always about the
clawboo CLI. A code change deep in @clawboo/db is still released as a CLI version bump; the lib change rides inside the CLI bundle, so the user-facing artifact that changed is the CLI.The CI gate
Every push tomain and every pull request runs .github/workflows/ci.yml. It is seven parallel jobs, all on Node 22 with pnpm install --frozen-lockfile (so the lockfile is authoritative; an out-of-sync lockfile fails the install):
The Prettier half of the
lint job runs first because it takes seconds and needs no build, whereas turbo lint builds every workspace dependency before it can run ESLint. It also means a formatting-only failure reddens the lint job while ESLint itself is green; the fix is pnpm format.
The smoke-test-bundle job is the one that earns its keep at release time, and it runs on a [ubuntu-latest, windows-latest, macos-latest] matrix. It first pnpm assembles the CLI bundle, then pnpm test:clean-install simulates npx clawboo on a real machine. Crucially, it does not run the repo build in place: it pnpm packs apps/cli and npm installs the tarball into a throwaway directory under the OS temp dir, so nothing resolves through the workspace’s node_modules and the published files whitelist plus the published dependency closure are what get exercised. Against that install it asserts the packaged bin entries and their npm shims exist, that every module the bundles still load is declared / builtin / documented-optional, that the CLI’s HTTP-signature port probe skips a fake non-Clawboo listener on 18791 (it picks 18790, never 18791), that the SPA renders at / and a deep route falls through to index.html, that /api/settings returns Clawboo-shaped JSON, that the SPA actually boots in headless Chromium rather than merely being served, that an installed MCP stdio bin completes a real JSON-RPC tools/list handshake, that a real POST /api/runtimes/clawboo-native/run drives a board task to done, and that a SECOND launch against an already-running dashboard reuses it instead of forking a second server. This exists because v0.1.1 shipped a Cannot GET / SPA-catch-all bug and v0.1.2 shipped a port-collision Unauthorized bug; the smoke test catches that whole class before a bundle reaches npm. The Windows leg is the regression gate for the v0.1.4 Windows-compat fixes (npm.cmd resolution, the which→where shim, netstat-based process lookup); the macOS leg covers a primary user OS. See Testing for the full assertion list.
The publish flow
.github/workflows/publish.yml runs on every push to main and implements the standard two-phase Changesets release: a first run opens a “Version Packages” PR, and merging that PR triggers a second run that publishes.
The workflow is a single publish job, and it runs unconditionally (see the warning below). It checks out with fetch-depth: 0 (Changesets needs the full git history to compute tags), installs frozen, pulls headless Chromium for the clean-install gate, then re-runs the whole PR gate in order: pnpm verify:catalog → pnpm build → pnpm lint → pnpm typecheck → pnpm test → bash scripts/assemble-cli.sh → pnpm test:clean-install, before the Changesets action. Finally it runs changesets/action@v1 with publish: pnpm changeset publish and commit: 'chore: version packages'.
Two details in that order are deliberate. pnpm build is bundler-only (Vite + tsup, no tsc), so a type error survives it — typecheck is the step that catches one, and without it a broken type could reach npm even though the build was green. And lint / typecheck / test run before assemble-cli.sh, so a failure costs no bundle work and no Turbo task can restore a stale apps/cli/dist over the freshly assembled one.
The changesets/action@v1 step is what gives the flow its two phases, deciding internally based on the repo state:
- Changesets present (a feature PR merged with a
.changeset/*.md) → the action opens (or updates) a Version PR titledchore: version packages. That PR, when merged, consumes the.mdfile, bumpsclawboo’s version inpackage.json, and writes theCHANGELOG.mdentry. - No changesets present (the Version PR itself just merged; Changesets already consumed the
.md) → the action runspnpm changeset publish, which publishes the CLI to npm (usingNODE_AUTH_TOKENfrom theNPM_TOKENsecret) and creates the git tag + GitHub release.
Publish provenance
The published tarball carries an npm provenance attestation: a signed statement that this tarball was built from this repository, at a specific commit, by this workflow. Two things inpublish.yml produce it, and both are load-bearing:
id-token: writein thepublishjob’spermissions:block. This is what lets the job mint a short-lived OIDC token from GitHub; npm verifies that token to establish who is publishing. It is the reason that job’s permission block is wider than the workflow’scontents: readdefault.NPM_CONFIG_PROVENANCE: truein thechangesets/action@v1step’senv:. This is the npm-side switch; without it the OIDC permission sits unused.
apps/cli/package.json must carry a repository field (it does, pointing at this repo with directory: apps/cli). npm refuses to generate provenance for a package that doesn’t say where it came from.
Anyone can check their own install:
npx and then spawns coding-agent runtimes on the user’s machine, so “is the code I’m about to run the code in the public repository?” is a question worth being able to answer without trusting us.
Releasing, step by step
The normal path to npm is:- Author a changeset.
pnpm changeset→ commit the generated.changeset/*.mdalongside your change on a feature branch; open a PR. - Pass CI. The seven jobs (
lint,typecheck,test,build,verify-catalog,smoke-test-bundle,e2e) must all be green, as must CodeQL. The bundle smoke test runs on Ubuntu, Windows, and macOS. - Merge the feature PR.
publish.ymlruns andchangesets/actionopens achore: version packagesVersion PR. - Merge the Version PR.
publish.ymlruns again:verify:catalog→build→lint→typecheck→test→assemble-cli.sh→test:clean-install→pnpm changeset publish. The CLI publishes, the tag and changelog land. - Verify.
npm view clawboo versionshould reflect the new version within roughly half a minute.
If a version is published manually (outside the Changesets flow) before the Changesets
version step has run, the repo state and npm diverge. The recovery is to run pnpm changeset version and commit it as chore: version packages so the changelog and tag catch up, not to re-run changeset publish for a version that is already live on npm.Design rationale and trade-offs
Why one published artifact? Clawboo’s value is the end-to-end product, not a constellation of reusable libraries. Keeping every@clawboo/* package private means there is no semver contract to maintain for two dozen internal packages, no internal publish-then-install loop, and no risk of a partial release where the CLI publishes against an unpublished lib. The cost is that the only way to consume a Clawboo library is to vendor the CLI bundle, which is exactly the intent. The packagePosture.test.ts guard turns “we only publish the CLI” from a habit into an enforced invariant.
Why inline the libs instead of declaring them as CLI dependencies? A clean npx clawboo install must run with no surprises. Inlining via tsup noExternal means a fresh machine needs only the CLI’s small set of genuinely-external runtime deps (better-sqlite3, ws, pino, pino-pretty, plus the lazily-imported optional ones); everything else, including the provider SDKs and the scheduler’s croner, is already in server.js. The clean-install smoke test is what proves this holds, and pnpm test:bundle-externals (also run inside that gate) is the static half: it extracts every require(...) / import(...) left in the shipped bundles and fails if one is neither declared, a Node builtin, nor on the documented-optional allowlist. That allowlist is deliberately tiny — @opentelemetry/* and @anthropic-ai/claude-agent-sdk, both lazy-imported and both degrading with an actionable message — because every entry is a thing a clean install cannot do until the user installs it themselves.
Why re-run the gate inside publish.yml? The PR already ran CI, but a merge race or an upstream-changed lockfile between PR-green and main-merge could ship a bundle that no longer assembles, or that assembles from code the PR gate never saw. Re-running lint, typecheck, tests, and the bundle smoke immediately before changeset publish makes a broken release impossible even if a PR slipped through, the same belt-and-suspenders reasoning that made the smoke test exist in the first place. The alternative, gating the publish job on the CI workflow via workflow_run or a required status check, was not taken: it would couple the release to a second workflow’s run history across the Version-PR merge, where inline steps keep the job self-contained and readable in one file.
Boundaries and non-goals
- Not a multi-package release. Despite the monorepo, there is no scenario where a
@clawboo/*library publishes independently. If you ever need that, it’s a deliberate posture change; start by readingpackagePosture.test.ts, which will fail the moment a second package goes non-private. - Not a hosted-deployment pipeline.
publish.ymlships an npm package; it does not deploy a service. Operating a running Clawboo instance is a separate concern; see Deployment. - No migration step at release. Clawboo has no database migration ladder; the schema is created by
ensureSchema’s DDL, and a user’s existing database is reconciled up to it the first time the new version opens it. There is nothing to run as part of a release. See Monorepo and build and Database schema.
These docs describe Clawboo v0.3.1, the current release.
See also
- Monorepo and build: Turbo, the build order, the bundle, and
assemble-cli.sh - Testing: the unit / component / e2e / clean-install / evals strategy behind the CI gate
- Codegen and ingestion: the offline
verify:cataloggate that runs in CI andpublish.yml, the liveverify:ingestcheck that runs weekly, and the runbook for bumping a pinned SHA - CLI reference:
npx clawbooand the bundled MCP bins - Changelog: the release history (0.1.0 → 0.3.1)
- Internals overview: the contributor map