docs/14-distribution.md

14 — Distribution

Picasso launches on two channels:

no npm provenance while the repository is private.

updates.picassohq.com.

Homebrew, the curl installer, Windows package managers, and Windows or Linux binaries are not shipped at launch (GO-LIVE-PRD §8).

Both launch channels can be built, but neither is published yet:

2026-10-28).

The @picasso npm scope belongs to another owner (veeronecorp gets 403 on it), so the CLI publishes under @picassohq. See docs/launch/platform-support.md for the verified matrix.

Channels

1. npm: bundled package built; publish pending

npm install -g @picassohq/cli

This command fails until the first publish. That needs two owner steps:

  1. The owner claims the @picassohq npm org and adds the GitLab trusted publisher.
  2. A cli-v<semver> tag triggers the publish job (see "GitLab release job" below).

What the package contains. scripts/release/package-npm.sh runs apps/cli/scripts/bundle.mjs, then scripts/release/create-npm-tarball.mjs, and writes dist/release/artifacts/picassohq-cli-<version>.tgz. The package is self-contained:

published package.json has no @picasso/* or workspace: dependency; the tarball script fails if one leaks. Third-party packages (commander, ink, react, yaml, …) stay normal dependencies.

(runtime/packages/<plugin>/). At startup the CLI sets its runtime asset root to <pkg>/runtime (PICASSO_RUNTIME_ASSETS_DIR overrides it). A missing overlay fails with picasso: Picasso runtime files are missing (<file>). Reinstall: npm i -g @picassohq/cli.

plus design/, assets/, agents/, goals/ and dist/build-info.json (commit, SOURCE_DATE_EPOCH, engine pin).

rebuild with the same SHA and SOURCE_DATE_EPOCH gives the same sha256.

package-npm.sh --publish runs npm publish <tgz> --access public --tag ${NPM_DIST_TAG:-latest} and reads or writes no npm token. With PICASSO_NPM_PROVENANCE=1 it adds --provenance, which works only inside the GitLab release job (OIDC NPM_ID_TOKEN). The provenance repository is gitlab.com/chahal-foundation-group/github-auraoneai/picasso. The workspace package itself stays "private": true, and its prepublishOnly refuses a workspace publish.

Engine runtime install. The CLI needs the pinned engine in ~/.picasso/dsh. It is installed from runtime-lock/ with npm ci into ~/.picasso/dsh, never from a version range. There are two paths:

saying it installed the pinned runtime 0.2.2-rc.1 into ~/.picasso/dsh/bin, on stderr. It skips in a repo checkout, with PICASSO_SKIP_DSH_INSTALL=1, on CI unless PICASSO_INSTALL_DSH=1, and when an engine of the pinned version is already on PATH. Every failure is non-fatal: the CLI still installs.

--dangerously-allow-all-scripts, so a plain npm i -g @picassohq/cli usually installs no engine, and a pin bump leaves an older ~/.picasso/dsh behind. The CLI (packaged or a repo checkout) therefore installs the pin itself, silently, on the first command that may need it whenever no engine at the pin is in use: missing, older, or only a different version on PATH (which is never used). It runs the same integrity-checked npm ci from the lock and prints nothing, unless it takes longer than about 2 s, when one line shows: Installing the Picasso engine runtime 0.2.2-rc.1…. It never prompts, and two launches at once run one install. This is skipped for --help, --version, update, auth, telemetry, help and doctor, with PICASSO_SKIP_DSH_INSTALL=1, and when PICASSO_DSH_RUNTIME_BIN is set. A failed attempt (offline) prints warning: <reason>. Retry with \picasso update runtime --install\. and is not retried for an hour (~/.picasso/runtime-install-attempt.json). The run then fails closed with the missing or mismatched engine error; an engine at another version is never used.

picasso update runtime --install runs the same install by hand at any time. Engine resolution: PICASSO_DSH_RUNTIME_BIN first, then ~/.picasso/dsh/bin/dsh when it is the pin, then the first engine on PATH that is the pin. A different engine version on PATH never wins over the pin, and a launch refuses a version that does not match.

2. Picasso for Mac: notarized DMG

Picasso.app/Picasso.dmg are built by apps/mac/Scripts/package-release.sh on the release Mac. They are arm64-only, signed with Developer ID, notarized, and sha256-summed in SHA256SUMS. Users download from https://picassohq.com/download. After that, Sparkle updates the app from https://updates.picassohq.com/mac/appcast.xml (see "R2 update host" below).

Install by asking your agent

The download page offers a prompt to paste into Picasso or any other coding agent. The agent downloads the release, checks its checksum, signature and notarization, reads What Picasso puts on your Mac, gives a short safety verdict, and only then installs. The same prompts are printed by picasso about --install-prompt mac and picasso about --install-prompt cli (/about prompt mac|cli in the terminal app), and the Mac one is in Picasso for Mac under Help › Install by asking your agent. apps/cli/src/footprint.ts holds the text; footprint.test.ts and WhatPicassoPutsOnYourMacTests fail if this page, the CLI or the Mac Help drift apart.

The marketing site's download page (apps/marketing/src/content/routes.ts, /install#mac-app) is kept out of this repository's commits, so the prompt lives here and in the Mac Help; copy it onto that page when the DMG goes live.

Picasso for Mac:

Install Picasso for Mac for me, but check it first. Do these in order and stop if any check fails:
1. Read https://updates.picassohq.com/mac/appcast.xml for the current version, then download https://updates.picassohq.com/mac/Picasso.dmg and https://updates.picassohq.com/mac/SHA256SUMS into a new empty folder.
2. Verify it: `shasum -a 256 Picasso.dmg` must match its line in SHA256SUMS; `xcrun stapler validate Picasso.dmg` must pass;
   mount it read-only, then `codesign --verify --deep --strict --verbose=2 /Volumes/Picasso/Picasso.app` must pass,
   `spctl --assess --type execute --verbose /Volumes/Picasso/Picasso.app` must say `accepted` and `source=Notarized Developer ID`,
   and `codesign -dv --verbose=4` must show Team ID 6T44QQW78K and identifier com.picassohq.picasso.
3. Read https://updates.picassohq.com/mac/what-picasso-puts-on-your-mac.md (what it installs, runs, reads and talks to)
   and compare it with what you find in the bundle (Contents/Resources, Contents/Library, any LaunchAgents or login items).
4. Give me a short safety verdict: what you checked, what passed, anything that surprised you, and install or don't install.
5. If the verdict is install, copy Picasso.app to /Applications, eject the disk image, and open it once.

The command line:

Install the Picasso command line for me, but check it first. Do these in order and stop if any check fails:
1. Run `npm view @picassohq/cli version dist.integrity dist.tarball` and note the version.
2. Download the tarball with `npm pack @picassohq/cli@<version>` into a new empty folder, and check its sha256 against https://updates.picassohq.com/cli/<version>/SHA256SUMS.
3. Verify the registry signatures: in a scratch folder, `npm install --ignore-scripts @picassohq/cli@<version>` then `npm audit signatures` must pass.
4. Read https://updates.picassohq.com/mac/what-picasso-puts-on-your-mac.md, especially "What the command line installs" (the scratch install prints the same list:
   `./node_modules/.bin/picasso about --footprint`), and read the package's postinstall script (scripts/install-runtime.mjs)
   to confirm it only installs the engine under ~/.picasso.
5. Give me a short safety verdict: what you checked, what passed, anything that surprised you, and install or don't install.
6. If the verdict is install, run `npm install -g @picassohq/cli@<version>` and then `picasso doctor`.

The Mac prompt expects three files beside the appcast on the update host: https://updates.picassohq.com/mac/Picasso.dmg, https://updates.picassohq.com/mac/SHA256SUMS (one <sha256> Picasso.dmg line per release) and https://updates.picassohq.com/mac/what-picasso-puts-on-your-mac.md. scripts/deploy/publish-mac-appcast.sh uploads only the Sparkle archives and the appcast today, so publishing these three is an open release step (see "Open questions").

Not shipped at launch

None of these are launch channels, and the marketing site and launch notes do not advertise them. The scaffolding stays in the repository:

is no install host.

scripts/release/generate-windows-manifests.sh refuses to emit publishable manifests without a signed artifact.

published. The npm CLI itself runs on Linux with Node 22+.

Signing and notarization

PlatformActual mechanism
macOSApple Developer ID signing + notarytool when credentials are configured; ad-hoc - signature otherwise (local-use only, Gatekeeper rejects it by design).
npmTrusted publishing (OIDC) from the GitLab job; no token anywhere. A Sigstore keyless bundle for the tarball sits next to SHA256SUMS on R2. npm provenance turns on only if the repository becomes public.
Windows, LinuxNot shipped; nothing is signed.

There is no GitHub Actions release pipeline: .github/workflows/* is retired (policy; docs/launch/ci-retirement-map.md). CLI release artifacts build on an ACR one-off lane (scripts/deploy/ci-lane.sh release <version>); the Mac app is built, signed and notarized on an owner-operated macOS host with apps/mac/Scripts/package-release.sh. The app ships non-sandboxed with the hardened runtime and is arm64-only. The script exits 1 without a signing identity unless --local asks for an ad-hoc build, and it would refuse a Developer ID build if a sandbox temporary-exception entitlement were ever added back without an Accepted notarization receipt. The Mac Developer ID identity lives in the release host's keychain, never in CI secrets. No HSM or hosted signing service exists in this repository.

Release tags

TagReleasesNotes
cli-v<semver>@picassohq/cliAnnotated, on main. A -beta.N suffix publishes to the npm dist-tag beta; anything else publishes to latest.
mac-v<semver>Picasso for MacCFBundleShortVersionString comes from the tag. CFBundleVersion is git rev-list --count <tag>, so it always increases on main.

The first public versions are cli-v1.0.0 and mac-v1.0.0. Server images are not tagged this way and stay sha-<12>.

GitLab release job (npm publish)

npm issues trusted-publishing tokens and provenance only to GitHub Actions or GitLab CI, and GitHub Actions is retired here. So the publish runs on the GitLab mirror gitlab.com/chahal-foundation-group/github-auraoneai/picasso, and only for release tags.

^cli-v[0-9]+\.[0-9]+\.[0-9]+. No branch push ever starts it. The mirror gets the tag from the normal GitHub→GitLab mirror sync.

and SIGSTORE_ID_TOKEN (aud: sigstore). It has no npm token variable. On npmjs.com, the package's trusted publisher is bound to that GitLab project and .gitlab-ci.yml. That binding is the signed-source identity, and the package is set to "Require 2FA and disallow tokens".

  1. Refuse unless the tagged commit is on main, then rebuild the tarball deterministically: RELEASE_VERSION=${CI_COMMIT_TAG#cli-v},

SOURCE_DATE_EPOCH=1704067200, scripts/release/package-npm.sh without --publish.

  1. Require its sha256 to equal the line in https://updates.picassohq.com/cli/<version>/SHA256SUMS.

The ACR release lane uploaded that line earlier (scripts/deploy/ci-lane.sh release <version>, then scripts/deploy/release-upload.sh).

  1. Refuse to republish a version that is already on npm with different bytes.
  2. Run npm publish <tgz> --access public --tag latest|beta.
  3. Wait until npm view shows the version, then write a npm-publish-<version>.json receipt.

owner publishes 1.0.0 once from the release Mac, then attaches the publisher. Every later version goes through this job.

node:22-bookworm-slim container and runs picasso --version and picasso doctor.

R2 update host (updates.picassohq.com)

Release artifacts that clients fetch live in Cloudflare R2, not in git or Vercel.

ItemValue
Bucketpicasso-updates (Cloudflare account that owns the picassohq.com zone)
Custom domainupdates.picassohq.com, TLS 1.2 minimum, no Worker, no cron
Mac appcasthttps://updates.picassohq.com/mac/appcast.xml (Sparkle SUFeedURL), EdDSA-signed enclosures under /mac/
CLI checksumshttps://updates.picassohq.com/cli/<version>/SHA256SUMS and the Sigstore bundle next to it
Createscripts/deploy/r2-updates-create.sh (idempotent)
Uploadscripts/deploy/release-upload.sh for CLI checksums; scripts/deploy/publish-mac-appcast.sh <dist/updates> for Mac zips and the appcast

Upload rules:

The bucket is permanent release infrastructure, not a temporary resource. The Sparkle EdDSA private key lives in OpenBao (lanes/bond/picasso-release/sparkle-ed25519). It is piped to the signing step and never written to disk.

Mac rollback. Republish the previous appcast entry, as a higher build number of the good code, with publish-mac-appcast.sh. Sparkle never downgrades, so a rollback is always a new build.

picasso update                       # check the npm registry for a newer release
picasso update --channel beta        # check the beta dist-tag
picasso update --install             # install it with npm (npm global installs only)
picasso update runtime               # report the pinned engine runtime
picasso update runtime --install     # install the pinned engine into ~/.picasso/dsh

The npm registry is the update feed. picasso update reads the @picassohq/cli dist-tags from npm_config_registry (default https://registry.npmjs.org): latest for stable (the default), and beta for --channel beta. It prints Picasso <cur> is up to date (<channel>). or Picasso <new> is available (you have <cur>). Run: npm i -g @picassohq/cli@<new>. With --install it runs that npm i -g itself, but only when the running CLI lives under npm root -g. Otherwise it prints the command and says this copy was not installed with npm i -g. Exit codes: 0 when current or updated, 1 on a registry, network or npm failure. The hidden --check-exit-code exits 10 when an update is available and was not installed. The hidden --manifest <path> reads a local update manifest for offline tests.

picasso update runtime prints the pin (0.2.2-rc.1), which engine is in use (the Picasso runtime, an engine on PATH, or PICASSO_DSH_RUNTIME_BIN) with its version, and a status of ok, mismatch or missing. When the status is not ok, it adds Run \picasso update runtime --install\ to install the pinned engine. The pin is the baseline (floor) a CLI release ships; the background engine updater may run a newer, compatibility-gated engine on top of it (below).

The minimum supported version comes from GET /v1/client-config (min_versions.cli, forced_update, update_required). Below the minimum, every command except update, auth, help, --help and --version exits 1 with Picasso <cur> is no longer supported (minimum <min>). Run: npm i -g @picassohq/cli@latest.

picasso update also runs one engine check right away: it stages a newer engine and puts it through the same gates as the background updater. The --runtime flag is the same as picasso update runtime.

The CLI itself never updates silently; only the engine runtime is kept current in the background. The CLI and the Mac app's relay share one updater: it checks every 6 hours, only while Picasso is idle, takes release-candidate and stable engines only, checks integrity and boots the new engine before trusting it, swaps it in atomically at the next start, and rolls back if the new engine fails. The signed Mac app keeps the engine bundled in it; Sparkle installs app updates silently (docs/mac-relay.md). Settings › General › Engine updates in the Mac app, and /updates in the terminal app, show the next check, a version waiting to switch, and recent results.

Rollback

A bad CLI release is rolled back on the registry, with no infrastructure:

npm dist-tag add @picassohq/cli@<prev> latest       # point `latest` back at the good release
npm deprecate @picassohq/cli@<bad> "<reason>"       # warn anyone who pins the bad version

From then on, picasso update reports <prev> as current for new checks. Users already on <bad> run npm i -g @picassohq/cli@<prev>. To force them off it, an admin raises min_versions.cli or sets forced_update in client config. These are owner-run npm commands. No script in this repository moves dist-tags. package-npm.sh --publish only sets the tag of the version being published (NPM_DIST_TAG, default latest; use NPM_DIST_TAG=beta for a beta).

Reproducible builds

Target: every release ships a build attestation verifying source commit, build environment (image, toolchain versions), commands, and output binary hashes. scripts/release/verify-reproducible.sh and generate-sbom-slsa.sh implement the mechanics; a separate rebuild-compare workflow is required before claiming SLSA Level 3 — today that level is not certified.

Version policy

Semver. Major version bumps for breaking changes to the CLI surface, the config file format, the hook interface, or the session schema. Minor for additive features. Patch for fixes.

Public-facing surfaces that count as breaking-change surfaces:

Internal surfaces (memory store schema, indexer format, sandbox profile) are versioned with internal migrations and are not breaking-change surfaces.

Migration on upgrade (target, not implemented)

No upgrade-migration code exists today: nothing in apps/ or packages/ creates ~/.picasso/migrations/. The behavior below is the design to build, not what the CLI does. When a new version requires data migration (new memory schema, new session log fields):

~/.picasso/migrations/<from-version>-<to-version>/ for 30 days.

roll back.

Telemetry of version distribution (target, not built)

No version-distribution metrics and no public stats dashboard exist. The intent is aggregated version-spread metrics on a public dashboard, so users on older versions can see they are behind and fixes can be prioritized for high-population versions.

Removal

npm rm -g @picassohq/cli                                # removes the CLI package
picasso uninstall --confirm uninstall                 # removes local data under ~/.picasso
picasso uninstall --keep-data                         # removes nothing; reports the plan
picasso uninstall --purge --confirm uninstall         # removes data and records a keychain purge request

picasso uninstall does not remove the package itself, and it does not delete Keychain items. It removes data and leaves the package and the keychain to the package manager or platform uninstaller. To sign out first, run picasso auth logout, which also signs out Picasso for Mac (docs/11-auth-and-accounts.md).

Acceptance criteria

These are the criteria for calling a channel shipped. No release has satisfied them yet except where docs/launch/platform-support.md records a receipt.

advertised anywhere.

files are a scaffold and real provenance attestation is an open gap (docs/launch/ci-retirement-map.md); no level is certified.

beta → stable), and the npm dist-tag add rollback rehearsed once after the first publish.

checkout) boots the TUI and passes the runtime asset check (the cli-install-smoke lane, H-I9).

Dependencies

not GitHub Actions.

Open questions

artifact; generation scripted, certification pending.)

with each release, so the install-by-agent prompt can verify the DMG. Today publish-mac-appcast.sh uploads only the Sparkle archives and the appcast.