Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/t12011-studio-dist-publish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
id: t12011-studio-dist-publish
tasks: [T12011]
kind: fix
summary: "publish pipeline builds+stages studio-dist before npm publish; tarball gate asserts every files[] entry exists"
---

Root cause: `packages/cleo/package.json` declares `studio-dist` in `files[]` but the release CI
pipeline never built `@cleocode/studio` before `pnpm publish`. npm silently omits files[] entries
that do not exist at publish time, so every `@cleocode/cleo` tarball shipped without `studio-dist`
from v2026.6.13 onward — causing `cleo web` to serve a blank Studio
(`E_STUDIO_BUNDLE_ABSENT` from every npm install).

## What changed

### `.github/workflows/release.yml`

Added "Build Studio bundle and stage into packages/cleo" step (runs after `pnpm run build`, before
any tarball verification or publish):
1. `pnpm --filter @cleocode/brain run build` — brain is a Studio runtime dep not in `build.mjs`.
2. `pnpm --filter @cleocode/studio run build` — SvelteKit adapter-node compilation.
3. `node packages/cleo/scripts/copy-studio-dist.mjs` — invokes the existing staging mechanism
from T11979 (copies `packages/studio/build/` to `packages/cleo/studio-dist/`).
4. Inline guard: exits non-zero if `packages/cleo/studio-dist/client` is absent after staging.

Added "Verify @cleocode/cleo tarball contents (T12011)" step that runs `node scripts/assert-cleo-tarball.mjs`.

### `scripts/assert-cleo-tarball.mjs` (new)

Loud tarball gate for `@cleocode/cleo`:
- Gate 1: every entry in `packages/cleo/package.json` `files[]` must exist on disk. Missing
entry = hard fail with `::error::` annotation naming the missing entry.
- Gate 2: `studio-dist/client/` and `studio-dist/client/_app/` must exist (the SvelteKit static
asset tree the gateway's `resolveStudioStaticDir()` expects).

### `packages/studio/package.json`

Added explicit dependencies so pnpm links them into studio's node_modules for the
SvelteKit adapter-node SSR server to resolve transitive imports at build time:
- `@cleocode/cant` (workspace:*) — transitively imported through `@cleocode/core`
- `@cleocode/caamp` (workspace:*) — transitively imported through `@cleocode/core`
- `jsonc-parser` (^3.3.1) — transitive dep of `@cleocode/caamp`

Full post-publish proof (cleo web serving a non-blank Studio from a fresh install) lands with
the next release after the Studio build resolves remaining runtime dep chain issues.
68 changes: 68 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,62 @@ jobs:
- name: Build
run: pnpm run build

# T12011: Build the Studio SvelteKit bundle and stage it into
# packages/cleo/studio-dist/ BEFORE any verification or publish step.
#
# Context: packages/cleo/package.json lists "studio-dist" in its files[]
# array, but build.mjs never builds @cleocode/studio (it is a SvelteKit
# app, not a TypeScript library). Without this step the packages/cleo/
# postbuild script (scripts/copy-studio-dist.mjs) silently exits 0
# because packages/studio/build/ does not exist in CI, which means
# studio-dist is absent at publish time and npm silently omits it from
# the tarball. cleo web then boots the gateway but serves a blank Studio
# from every npm install.
#
# The Studio build is a SvelteKit adapter-node compilation and is the
# only heavyweight step outside build.mjs. No incremental caching is
# possible here — always rebuild from scratch.
# T12011: Build the Studio SvelteKit bundle and stage it into
# packages/cleo/studio-dist/ BEFORE any verification or publish step.
#
# Context: packages/cleo/package.json lists "studio-dist" in its files[]
# array, but build.mjs never builds @cleocode/studio (it is a SvelteKit
# app, not a TypeScript library). Without this step the packages/cleo/
# postbuild script (scripts/copy-studio-dist.mjs) silently exits 0
# because packages/studio/build/ does not exist in CI, which means
# studio-dist is absent at publish time and npm silently omits it from
# the tarball. cleo web then boots the gateway but serves a blank Studio
# from every npm install.
#
# Build order:
# 1. @cleocode/brain — depends on contracts+core+paths (all built by
# build.mjs above). brain is a Studio runtime dependency that
# build.mjs does not include in its wave graph.
# 2. @cleocode/studio — SvelteKit adapter-node build. Depends on
# brain, core, runtime (all now present after step 1).
# 3. copy-studio-dist.mjs — copies packages/studio/build/ into
# packages/cleo/studio-dist/ (the existing staging mechanism from
# T11979/#1074).
- name: Build Studio bundle and stage into packages/cleo
run: |
set -euo pipefail

echo "--- Building @cleocode/brain (Studio runtime dep, not in build.mjs) ---"
pnpm --filter @cleocode/brain run build

echo "--- Building @cleocode/studio (SvelteKit adapter-node) ---"
pnpm --filter @cleocode/studio run build

echo "--- Staging studio build → packages/cleo/studio-dist ---"
node packages/cleo/scripts/copy-studio-dist.mjs

echo "--- Verifying studio-dist was staged ---"
if [[ ! -d "packages/cleo/studio-dist/client" ]]; then
echo "::error::packages/cleo/studio-dist/client not found after staging"
exit 1
fi
echo "studio-dist/client present after staging"

# Hard gate: ESM module-load smoke test — runs AFTER build, BEFORE publish.
#
# Imports @cleocode/core and @cleocode/cleo directly from the local built
Expand Down Expand Up @@ -358,6 +414,18 @@ jobs:
fi
echo "Playbooks tarball verification passed"

# T12011: Hard gate — verify that @cleocode/cleo's npm tarball contains
# every entry declared in packages/cleo/package.json files[] AND that
# the Studio bundle (studio-dist/client/) is present with at least an
# index entry. A silently-thinner cleo tarball must never ship again.
#
# This gate runs AFTER the "Build Studio bundle and stage" step above, so
# studio-dist should already be populated. If it is missing, the gate
# fails loudly with the missing entry name rather than silently omitting
# it from the published package.
- name: Verify @cleocode/cleo tarball contents (T12011)
run: node scripts/assert-cleo-tarball.mjs

- name: Validate build output
run: |
declare -a required_artifacts=(
Expand Down
7 changes: 7 additions & 0 deletions docs/plan/dogfood-harness-question-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -1300,3 +1300,10 @@ Owner surface: CORE OAuth login — process exit after token storage.
Observed: after `cleo login anthropic` completes the browser-approve → token-exchange flow and stores the `sk-ant-oat` credential, the process hangs indefinitely. The user must Ctrl-C to return to the shell. The token IS stored (subsequent `cleo llm test` works), but the CLI process never calls `process.exit()` after the flow completes. Likely cause: an open handle (event emitter, timer, or HTTP server from the OAuth callback listener) that is not torn down after token storage.

Answer vehicle: identify the open handle via `--expose-gc` / `wtfnode` in dev, ensure the OAuth callback HTTP server is `server.close()`d and all timers are cleared after the token is stored, and add a process exit path (`process.exit(0)`) as a final backstop if handles remain open after a 500 ms grace period. Regression test: run `cleo login anthropic` with a mock token-exchange endpoint and assert the process exits within 2 seconds of the exchange completing.
### DHQ-098 — published tarball silently ships without studio-dist (npm skips missing files[] entries) — **FIXED in this PR** (T12011)

Owner surface: CORE release pipeline — `release.yml` build + publish phase.

Observed auditing the installed v2026.6.15 package: `packages/cleo/package.json` `files[]` includes `"studio-dist"` but the installed `@cleocode/cleo` has no `studio-dist/` directory. npm silently omits `files[]` entries that do not exist at publish time. T11979/#1074 added the entry and the `copy-studio-dist.mjs` staging script but the `release.yml` publish job never built `@cleocode/studio` — so `packages/cleo/studio-dist/` was never created in CI, and npm silently dropped it. `cleo web` boots the gateway (post-T12009) but `/studio` returns `E_STUDIO_BUNDLE_ABSENT` from every global install.

Answer vehicle: (1) add an explicit "Build Studio bundle and stage into packages/cleo" CI step running `@cleocode/brain` + `@cleocode/studio` builds + `copy-studio-dist.mjs` before any publish step; (2) new `scripts/assert-cleo-tarball.mjs` gate that asserts every `files[]` entry exists on disk and that `studio-dist/client/_app/` is present — hard fail with `::error::` annotation on any missing entry so the regression can never ship silently again. Also add missing transitive deps (`@cleocode/cant`, `@cleocode/caamp`, `jsonc-parser`) to `packages/studio/package.json` so pnpm links them for the SvelteKit adapter-node SSR build.
3 changes: 3 additions & 0 deletions packages/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"dependencies": {
"3d-force-graph": "^1.80.0",
"@cleocode/brain": "workspace:*",
"@cleocode/cant": "workspace:*",
"@cleocode/caamp": "workspace:*",
"@cleocode/contracts": "workspace:*",
"jsonc-parser": "^3.3.1",
"@cleocode/core": "workspace:*",
"@cleocode/runtime": "workspace:*",
"loro-crdt": "*",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

134 changes: 134 additions & 0 deletions scripts/assert-cleo-tarball.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env node
/**
* T12011: Assert that the @cleocode/cleo npm tarball will contain every entry
* declared in packages/cleo/package.json `files[]` AND that the Studio bundle
* (studio-dist/client/) is present with a non-empty asset tree.
*
* WHY THIS EXISTS
* ---------------
* npm silently omits `files[]` entries that do not exist on disk at publish
* time. When T11979/#1074 merged Studio-in-package, `packages/cleo/package.json`
* gained `"studio-dist"` in its `files[]` array. But the release CI pipeline
* never built @cleocode/studio before running `pnpm publish`, so
* `packages/cleo/studio-dist/` was never created and npm silently omitted it.
* The published @cleocode/cleo tarball shipped without Studio from v2026.6.13
* onward; every `cleo web` session served a blank Studio.
*
* This script catches that regression before `pnpm publish` runs:
* 1. Reads `packages/cleo/package.json` `files[]`.
* 2. Asserts every entry exists on disk as a file or directory.
* 3. Asserts `packages/cleo/studio-dist/client/` exists (the path the gateway
* calls `resolveStudioStaticDir` expects — see
* `packages/runtime/src/gateway/http/studio-static.ts`).
* 4. Asserts `packages/cleo/studio-dist/client/_app/` exists (the SvelteKit
* immutable-asset tree — its absence means the build is partial).
*
* Exit code 0 = all assertions passed.
* Exit code 1 = one or more assertions failed (each failure is printed to stderr
* with a `::error::` annotation so GitHub Actions surfaces it as a step failure
* with the entry name highlighted).
*
* @task T12011
* @epic T11261
*/

import { existsSync, readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');
const CLEO_PKG_DIR = join(REPO_ROOT, 'packages', 'cleo');
const CLEO_PKG_JSON = join(CLEO_PKG_DIR, 'package.json');

// ---------------------------------------------------------------------------
// Load packages/cleo/package.json
// ---------------------------------------------------------------------------

/** @type {{ files?: string[] }} */
let pkg;
try {
pkg = JSON.parse(readFileSync(CLEO_PKG_JSON, 'utf8'));
} catch (err) {
console.error(`::error::assert-cleo-tarball: cannot read ${CLEO_PKG_JSON}: ${err.message}`);
process.exit(1);
}

const filesEntries = pkg.files ?? [];
if (filesEntries.length === 0) {
console.warn('assert-cleo-tarball: packages/cleo/package.json has no files[] — skipping.');
process.exit(0);
}

// ---------------------------------------------------------------------------
// Gate 1: every files[] entry must exist on disk
// ---------------------------------------------------------------------------

let failed = false;

console.log('[assert-cleo-tarball] Checking packages/cleo/package.json files[] entries...');
for (const entry of filesEntries) {
const absPath = join(CLEO_PKG_DIR, entry);
if (!existsSync(absPath)) {
console.error(
`::error::assert-cleo-tarball: files[] entry "${entry}" is MISSING on disk at ${absPath}. ` +
`npm will silently omit this from the published tarball. ` +
`Ensure the build step that produces this entry runs BEFORE publish.`,
);
failed = true;
} else {
console.log(` OK ${entry}`);
}
}

// ---------------------------------------------------------------------------
// Gate 2: studio-dist/client/ must contain SvelteKit static assets
//
// The gateway resolves `studio-dist/client/` via `resolveStudioStaticDir()`
// (packages/runtime/src/gateway/http/studio-static.ts). If this directory is
// absent or empty, every `cleo web` session serves E_STUDIO_BUNDLE_ABSENT.
// ---------------------------------------------------------------------------

const studioDist = join(CLEO_PKG_DIR, 'studio-dist');
const studioClient = join(studioDist, 'client');
const studioApp = join(studioClient, '_app');

console.log('\n[assert-cleo-tarball] Checking studio-dist layout...');

if (!existsSync(studioDist)) {
console.error(
`::error::assert-cleo-tarball: studio-dist/ directory is MISSING at ${studioDist}. ` +
`Run \`pnpm --filter @cleocode/studio run build\` and then ` +
`\`node packages/cleo/scripts/copy-studio-dist.mjs\` before publishing.`,
);
failed = true;
} else if (!existsSync(studioClient)) {
console.error(
`::error::assert-cleo-tarball: studio-dist/client/ is MISSING at ${studioClient}. ` +
`The SvelteKit adapter-node build places static assets in build/client/; ` +
`copy-studio-dist.mjs copies the full build/ tree, so client/ must be present. ` +
`Re-run the Studio build and staging step.`,
);
failed = true;
} else if (!existsSync(studioApp)) {
console.error(
`::error::assert-cleo-tarball: studio-dist/client/_app/ is MISSING at ${studioApp}. ` +
`This directory contains the SvelteKit immutable asset tree. ` +
`The Studio build may be partial or corrupted. Re-run the Studio build.`,
);
failed = true;
} else {
console.log(` OK studio-dist/client/ (SvelteKit static assets present)`);
console.log(` OK studio-dist/client/_app/ (immutable asset tree present)`);
}

// ---------------------------------------------------------------------------
// Summary
// ---------------------------------------------------------------------------

if (failed) {
console.error('\n[assert-cleo-tarball] FAILED — see errors above.');
process.exit(1);
}

console.log('\n[assert-cleo-tarball] All assertions passed — @cleocode/cleo tarball is complete.');
Loading