Skip to main content
A Clawboo release is the publish of one npm package: the 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 is clawboo@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:
A few of these are load-bearing:
  • 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 as chore: 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 internal workspace:* 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.
This walks you through which package changed (in practice, 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 to main 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 whichwhere shim, netstat-based process lookup); the macOS leg covers a primary user OS. See Testing for the full assertion list.
You can reproduce the release gate locally before authoring a changeset. pnpm prepublish:check is the alias for pnpm verify:catalog && pnpm assemble && pnpm test:clean-install — the catalog-and-artifact slice of the release gate, not the whole of it: publish.yml also runs build, lint, typecheck and test, so run those separately (or rely on the PR jobs) before cutting a release. If it fails locally, the release is broken; fix it before opening the PR. It drives the SPA in headless Chromium, so run pnpm exec playwright install chromium once on a fresh clone; the smoke fails with that exact command in the message if the browser is missing.

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:catalogpnpm buildpnpm lintpnpm typecheckpnpm testbash scripts/assemble-cli.shpnpm 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 titled chore: version packages. That PR, when merged, consumes the .md file, bumps clawboo’s version in package.json, and writes the CHANGELOG.md entry.
  • No changesets present (the Version PR itself just merged; Changesets already consumed the .md) → the action runs pnpm changeset publish, which publishes the CLI to npm (using NODE_AUTH_TOKEN from the NPM_TOKEN secret) 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 in publish.yml produce it, and both are load-bearing:
  • id-token: write in the publish job’s permissions: 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’s contents: read default.
  • NPM_CONFIG_PROVENANCE: true in the changesets/action@v1 step’s env:. This is the npm-side switch; without it the OIDC permission sits unused.
A third requirement lives outside the workflow: 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:
This matters more here than for a typical library. Clawboo is installed with 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.
The publish job has no changeset gate, and must not get one. Counting .changeset/*.md and adding if: has_changesets == 'true' looks right, and is right for the “open a Version PR” run (changesets are present), but it would block the publish step on the Version-PR-merge run; at that point Changesets has already consumed the .md file, so the count is zero and the gate would skip the very run that’s supposed to publish. changesets/action@v1 handles both phases internally; it just needs the job to run unconditionally.The workflow used to carry a separate check job that computed exactly that count. The if: was removed once the trap was hit, which left the job wired in by needs: but feeding nothing; it has since been deleted so the workflow’s intent reads plainly. A comment in publish.yml records the same reasoning at the point where someone would be tempted to re-add it.

Releasing, step by step

The normal path to npm is:
  1. Author a changeset. pnpm changeset → commit the generated .changeset/*.md alongside your change on a feature branch; open a PR.
  2. 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.
  3. Merge the feature PR. publish.yml runs and changesets/action opens a chore: version packages Version PR.
  4. Merge the Version PR. publish.yml runs again: verify:catalogbuildlinttypechecktestassemble-cli.shtest:clean-installpnpm changeset publish. The CLI publishes, the tag and changelog land.
  5. Verify. npm view clawboo version should 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 reading packagePosture.test.ts, which will fail the moment a second package goes non-private.
  • Not a hosted-deployment pipeline. publish.yml ships 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:catalog gate that runs in CI and publish.yml, the live verify:ingest check that runs weekly, and the runbook for bumping a pinned SHA
  • CLI reference: npx clawboo and the bundled MCP bins
  • Changelog: the release history (0.1.0 → 0.3.1)
  • Internals overview: the contributor map
Last modified on August 10, 2026