Skip to content

feat(react): publish native Cezar cockpit facade - #931

Open
andrzejewsky wants to merge 35 commits into
mainfrom
codex/cezar-cockpit-facade
Open

andrzejewsky wants to merge 35 commits into
mainfrom
codex/cezar-cockpit-facade

Conversation

@andrzejewsky

@andrzejewsky andrzejewsky commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Export a coarse, publishable @open-mercato/cezar-react/cockpit facade that composes the existing complete Cezar application instead of rebuilding features one by one.
  • Add instance-scoped credentialed client/provider runtime, controlled memory routing, scoped CSS, fonts, and provider-owned portals for iframe-free embedding.
  • Add packed-consumer, boundary, CSS/font, and real-Chrome coverage, including native task creation and full cockpit navigation.

Motivation

Sandbox hosts need to embed the complete Cezar cockpit as React components without an iframe while preserving existing task, session, Git, skills, workflows, and settings functionality.

Sandbox usage

Import the stylesheet once in the host application, construct a credentialed client for the sandbox-specific Cezar origin, and use memory routing so Cezar navigation does not replace the outer sandbox URL.

"use client"

import { useEffect, useState } from "react"
import { createCezarClient } from "@open-mercato/cezar-api-client"
import { createCezarQueryClient } from "@open-mercato/cezar-react"
import { CezarCockpit } from "@open-mercato/cezar-react/cockpit"
import "@open-mercato/cezar-react/styles.css"

type SandboxCezarProps = {
  sandboxId: string
  cezarUrl: string
  renewAccess: () => Promise<unknown>
}

export function SandboxCezar({
  sandboxId,
  cezarUrl,
  renewAccess,
}: SandboxCezarProps) {
  const origin = new URL(cezarUrl).origin

  return (
    <SandboxCezarInstance
      key={JSON.stringify([sandboxId, origin])}
      origin={origin}
      renewAccess={renewAccess}
    />
  )
}

function SandboxCezarInstance({
  origin,
  renewAccess,
}: {
  origin: string
  renewAccess: () => Promise<unknown>
}) {
  const [client] = useState(() =>
    createCezarClient({
      baseUrl: origin,
      credentials: "include",
    }),
  )
  const [queryClient] = useState(() => createCezarQueryClient())
  const [path, setPath] = useState("/")

  useEffect(() => () => queryClient.clear(), [queryClient])

  return (
    <div className="h-full min-h-0 min-w-0">
      <CezarCockpit
        client={client}
        queryClient={queryClient}
        routing={{
          mode: "memory",
          path,
          onPathChange: setPath,
        }}
        onAuthRequired={async (error) => {
          if (error.status !== 401) return
          await renewAccess()
          await queryClient.invalidateQueries({ refetchType: "active" })
        }}
        className="size-full min-h-0 min-w-0"
      />
    </div>
  )
}

Sandbox integration requirements:

  • Derive baseUrl from the authorized Cezar app URL returned for that sandbox; do not use a process-global API URL.
  • Use credentials: "include" so the sandbox access cookie is sent to HTTP and live-event endpoints.
  • Keep one cockpit runtime mounted per workspace. When switching dock or split layouts, move or hide its mount instead of remounting it so task/session state is retained.
  • Key the runtime by sandbox plus Cezar origin so changing authority creates a clean client and query cache.
  • Give the host panel an explicit height with min-height: 0; the cockpit fills its parent.
  • routing.mode: "memory" preserves the outer sandbox URL while the controlled path can be retained by the host.

Validation

  • Focused cockpit/runtime suite: 55/55 passing.
  • Pack verifier suite: 12/12 passing.
  • Cold installed consumer: contract, client, and React tarballs typecheck and Vite-build with scoped CSS and all four font assets.
  • Real Chrome facade flow: passing; only Web and Editor remain iframes.
  • Main CI on the final merge tree: passing, including unit, build, E2E, and package gates.
  • Snapshot publication is currently blocked by repository-side npm publishing configuration; see docs/publishing.md for the maintainer setup.

Notes

  • This PR intentionally does not add npm publication or release automation.
  • The first consumer is the separate mercato-sandboxes native-cockpit integration.
  • Before claiming generic simultaneous multi-authority host support, the legacy transport-base compatibility bridge should be hardened for concurrent cockpit owners.

@andrzejewsky

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-review-pr — re-review result

Decision: CHANGES REQUESTED

The source-level release defect found in this review is fixed in ff81d86a (fix(release): publish React cockpit facade): the public @open-mercato/cezar-react facade is now stamped, dependency-pinned, and published in both stable and snapshot release order. Documentation and unit/E2E coverage were updated with it.

Validation passed locally: npm run typecheck, npm test, npm run test:unit, npm run build, and npm run test:package. The fresh required Unit, build, E2E, and package CI job also passed.

Remaining blocker is external CI configuration: Publish npm snapshot fails while publishing the first package, @open-mercato/cezar-contract, with npm E404 (package not found or no permission). The run confirms the corrected release stamp includes @open-mercato/cezar-react before that failure. An npm organization owner must grant the CI token create/publish access for the @open-mercato scope, then rerun the job.

GitHub does not allow an author to submit a request-changes review on their own PR, so this disposition is recorded by this comment and the changes-requested label.

@andrzejewsky

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-review-pr completed: CHANGES REQUESTED. Lock released.

Source review is clean after ff81d86a; PR #931 remains blocked only by the required npm snapshot-publish permission failure described above.

@andrzejewsky
andrzejewsky marked this pull request as ready for review August 27, 2026 18:36
@pat-lewczuk pat-lewczuk added the in-progress Cezar agent is actively working this issue label Aug 28, 2026
@pat-lewczuk pat-lewczuk self-assigned this Aug 28, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr started by @pat-lewczuk at 2026-08-28T21:57:51Z. Other auto-skills will skip this PR until the lock is released.

Note: the only pre-existing claim signal was the author assignee left by the previous run's changes-requested handoff (that run posted Lock released. on 2026-08-26T19:54Z and no in-progress label remained), so this is not an override of a live lock.

@pat-lewczuk pat-lewczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Code Review

🎯 Summary

PR #931 introduces @open-mercato/cezar-react — a coarse, publishable facade that composes the existing cockpit for iframe-free embedding — and, to make that installable, flips @open-mercato/cezar-contract and @open-mercato/cezar-api-client from private to public. The engineering underneath is strong: the instance-scoped client, the provider-owned portal surface, the adopted-root appearance contract that restores exactly what it changed, the CSS scoper with its own verifier, and roughly 1,400 lines of new focused tests. The release-set defect from the previous review pass is genuinely fixed in ff81d86areact is now stamped, dependency-pinned, and published in both channels, with tests covering it.

What is not ready is the delivery around that code. Three brand-new verification gates ship without ever being wired into CI; the CI job that vouches for publishable tarballs still vouches for the old two-package release set; the package advertises two public subpath exports that are empty files; and publishing three new packages into a scope the CI token cannot write to turns a required check red for every pull request in this repository, not just this one. None of these are in the facade itself — they are in the contract this PR is asking the project to take on permanently.

🧭 Verdict

Request changes. Two blockers (the publish failure that breaks CI for every future PR and the stable release path, and the missing compatibility record for three newly-public packages) and six majors (two unenforced gates, a stale release-pack gate, two empty public exports, and two runtime-correctness issues in the new public API).

🧪 Validation Gate

Run in an isolated worktree at the PR head ff81d86a, npm ci restored from the lockfile.

Command Result Evidence
npm run typecheck ✅ Pass Contract, API client, React, service, and web typechecks all clean. See the tsconfig.test.json finding below for what this gate does not cover.
npm test ⚠️ Fails in my sandbox only — not attributable to this PR 8 failed / 6275 passed on the first run, 12 failed / 6271 passed on an identical re-run: non-deterministic, and no failing file appears in this diff. The cause is my environment, not the code: os.tmpdir() here resolves to /home/cezar/cezar/.ai/cezar/tmp/…, which is inside the repository, so every test that builds a temp dir and asserts the absence of a git repo fails on a git rev-parse that walks up and finds one — git.test.ts "returns null outside a git repository", git-worktree.test.ts "answers null when the path is not a git worktree", git-changes.test.ts "commitAll on a non-repo dir", and the health/parity suites that boot a server in such a dir. The rest are ENOENT races from RunManager.rescueStalledQueue writing into already-torn-down temp dirs. CI's equivalent job is green on this head. I am recording this so the gate result is honest, not as a finding.
npm run test:unit ✅ Pass 36 passed, 0 failed.
npm run build ✅ Pass All five workspaces built; scope-css, verify-css, verify-cockpit-artifact and check:pack all ran and passed (check:pack ok — 475 files, 85 under web/dist).
npm run test:package ✅ Pass 15 passed, including the new release-order coverage.

⛔ Blockers

  • [blocker] .github/workflows/ci.yml:71 — publishing three new packages into an unwritable npm scope breaks the snapshot job for every pull request in the repository, and breaks the stable release path too. Publish npm snapshot is red on this head: npm publish returns E404 for @open-mercato/cezar-contract after provenance creation, because the Actions token has no create/publish rights on the @open-mercato scope.

    One correction to the previous review pass on this PR, which called this a required check: it is not. main has no required status checks — the only ruleset on it is pull_request with required_approving_review_count: 1, which is why mergeStateStatus reads BLOCKED. So this failure does not itself gate the merge button.

    It is still a blocker, for a larger reason than this PR: publish-snapshot runs on every same-repo pull request (github.event.pull_request.head.repo.full_name == github.repository), and scripts/release-snapshot.mjs:113 degrades to a dry run only when NPM_TOKEN is absent — never when the token is present but lacks scope rights. The same is true of scripts/release.mjs:99. So merging as-is leaves every future PR in this repo with a red CI badge and leaves the maintainer's stable npm run release failing on the first package it tries to publish, until an npm organization owner grants scope-level access. Either land the org permission before merging, or teach both scripts to treat an E404/E403 on a not-yet-created scoped package as the same loud dry run they already have for a missing token. docs/publishing.md step 3 documents the manual setup, but nothing in code degrades without it.

  • [blocker] BACKWARD_COMPATIBILITY.md — three packages become public npm surfaces and the compatibility document gains no section, which §6 explicitly requires before this can happen. Section 6 says of the library surface: "There is no exports/library API — the package is CLI-only. Keep it that way deliberately: adding one creates a new compatibility surface; if it happens, this document gains a section first." This PR creates three such surfaces at once — @open-mercato/cezar-contract, @open-mercato/cezar-api-client (both flipped out of private), and @open-mercato/cezar-react with its four subpath exports and styles.css — and the document is untouched. Compounding it, docs/publishing.md:30 deletes the recorded rationale for keeping the api-client private ("it still carries the hand-written DTOs, which shrink family by family as routes are converted, so publishing now would advertise a contract that changes materially every release") and replaces it with a statement that the set is public, without arguing anywhere that the stated precondition — the surface has stopped moving — is now met. The api-client still carries those hand-written DTOs on this branch. CODE_REVIEW.md's severity guidance makes breaking a BACKWARD_COMPATIBILITY.md surface without the required path a blocker. Required path: add the section describing what is now frozen in each of the three packages (the exports maps, CezarClient/CezarProvider/CezarCockpit prop shapes, the styles.css entry, the .cezar-root class and data-cezar-* attribute contract), and either argue the api-client's surface has settled or say explicitly which parts are provisional.

⚠️ Majors

  • [major] .github/workflows/ci.yml (unchanged) vs package.json:38npm run test:cockpit-package is never run by anything. The PR adds 533 lines of packed-consumer verification (scripts/check-cockpit-pack.mjs + scripts/check-cockpit-pack.test.mjs) and a fixtures/cockpit-consumer project, wires them to a test:cockpit-package script, and then never references that script from a workflow, from npm run build, from test:package, or from .ai/agentic.config.json's validation gate. This is the one gate that exercises the PR's actual headline delivery path — pack the tarballs, cold-install them in a fresh consumer, typecheck and Vite-build it, and scan the installed runtime for #cezar-web-cockpit / packages/web / cockpit-implementation leaking into shipped .js and .d.ts (scripts/check-cockpit-pack.mjs:26). I ran it by hand in the review worktree and it passes cleanly — cold cockpit consumer ok — 3 tarballs installed, 1 typecheck, 1 Vite build, 4 fonts resolved, 70 runtime/declaration files scanned — and the built dist/*.d.ts carry no private markers. That is exactly why it needs to run automatically: it is correct and green today, and nothing will notice the day it stops being. Add it as a CI step.

  • [major] packages/react/package.json:52check:boundaries is likewise never run. scripts/check-import-boundaries.mjs walks src/ and rejects imports of packages/web, @/…, @open-mercato/cezar-contract, and node: builtins — the invariant that keeps the published package from reaching into the private app or into Node. Its unit test (check-import-boundaries.test.mjs) is picked up by the react vitest config, but that test only exercises findProhibitedSpecifiers against string fixtures; it never scans the real source tree. So the guard is tested and not applied. It passes when run by hand (npm run check:boundaries -w @open-mercato/cezar-react, exit 0) — same argument as above: wire it into build or CI.

  • [major] .github/workflows/ci.yml:60-68 — the "Verify release packages" step still vouches for the old two-package release set. It packs only @open-mercato/cezar and alias-cezar, and its comment still explains the omission as "the packages marked private (the cockpit SPA and, for now, the api-client)". After this PR three more packages publish, each with a hand-written files/exports/main/types block that has never been pack-verified. A wrong files entry in packages/react/package.json:9 (["dist", "licenses", "README.md"]) or a stale exports target would now be discovered by the publish step rather than by the gate that exists to catch it. Extend the step to npm pack --dry-run the contract, api-client and react workspaces, and update the comment, which is now factually wrong.

  • [major] packages/react/package.json:14-15./tasks and ./session are published subpath exports whose entry files are empty. packages/react/src/tasks.ts and src/session.ts are both literally export {}; vite.config.ts:41-42 builds them as entry points, and the built artifacts confirm it — dist/tasks.js and dist/session.js are 0 bytes, dist/tasks.d.ts and dist/session.d.ts are export {};. A consumer writing import { … } from '@open-mercato/cezar-react/tasks' gets a silently-successful import that resolves nothing. Published exports entries are precisely the surface the blocker above is about: once a consumer depends on the path, removing it is a breaking change with a deprecation window. Either drop both from exports, vite.config.ts and the source tree until they have content, or make them throw a named "not implemented yet" error so the failure is loud.

  • [major] packages/react/src/cockpit.tsx:48-52useLegacyTransportBaseUrl mutates module-level state during render. activeLegacyTransportOwner = owner.current and setApiBaseUrl(baseUrl) run in the hook body, not in an effect. Two consequences. First, render-phase side effects are unsafe under React 19: a render that is discarded (Suspense, an interrupted concurrent render, StrictMode's double invoke) still mutates the module global, so the process-wide API base can be left pointing at a client whose tree never committed. Second — and this is the one an embedder will hit — the global is last-render-wins, so with two CezarCockpit instances mounted (the multi-sandbox layout the PR description itself describes), any re-render of instance A silently repoints the private composition's HTTP and workspace-SSE URLs at A's origin while B's queries are in flight. The PR notes and the README both acknowledge that only one instance is supported, but nothing in the code enforces or warns about it. At minimum: move the mutation into a layout effect, and emit a development-mode warning when a second owner claims the lease, so the failure is diagnosable instead of presenting as B fetching A's data.

  • [major] packages/api-client/src/client.ts:96 and packages/react/src/core/storage.ts:7CezarClient.identity is a construction-order counter, and it namespaces localStorage keys. identity is cezar-client-${++nextClientIdentity} off a module-level counter, and createCezarBrowserStorage builds keys as cezar:${identity}:${projectId}:${key}. Failure scenario: a host mounts cockpit A (identity cezar-client-1) then cockpit B (cezar-client-2), and each writes a draft through useCezarRuntime().storage. On the next page load the host renders B first — B is now cezar-client-1 and reads A's persisted draft, while A's own values are unreachable. Nothing about the identity is tied to the authority it actually represents. The tests do not catch this because they construct fakeCezarClient('client-a') with a literal identity (packages/react/src/core/provider.test.tsx:331), so the real identity is never exercised — and provider.test.tsx:212 asserts a "stable cache namespace" against that fake, which reads as coverage for exactly the property the implementation does not have. Derive identity from something stable and meaningful — the normalized baseUrl plus the credential mode — and keep the counter only as a disambiguator for two clients that are genuinely identical.

🔽 Minors

  • [minor] packages/react/tsconfig.test.json:6 — the typecheck gate skips every .tsx test file. "include": ["src/**/*.ts"] with "exclude": [] is unmistakably intended to add test files back into the check, but TypeScript's *.ts glob does not match .tsx, so src/cockpit.test.tsx (333 lines) and src/core/provider.test.tsx (406 lines) are outside npm run typecheck. Confirmed with tsc --noEmit -p tsconfig.test.json --listFiles: zero .test.tsx files in the program. Change the include to ["src/**/*.ts", "src/**/*.tsx"].

  • [minor] packages/react/src/styles/base.css and packages/react/src/styles/tokens.css are unreferenced. 121 lines defining a --cezar-* token system and a .cezar-root preflight block, and nothing imports them — src/styles/index.css only pulls in ../../../web/src/styles/index.css. The shipped stylesheet uses the web app's --background/--foreground names, not --cezar-background, so these are a competing token vocabulary left over from an earlier approach. Please confirm the drop was intentional (the root element does lose Tailwind preflight's box-sizing/border-color, which base.css was written to restore — harmless today because the root carries no padding or border, but it is the kind of thing that is easier to reason about deleted than dormant) and remove them.

  • [minor] packages/react/package.json:24 and packages/react/vite.config.ts:14-17react-router is a declared runtime dependency but is bundled into dist. isReactPackageExternal externalizes only react, react-dom, @open-mercato/cezar-api-client and @tanstack/react-query, so react-router is inlined into the artifact; the built dist/cockpit.js has no bare react-router import. Every consumer therefore installs a copy of react-router that is never loaded. Either add it to runtimeDependencies (and accept host dedup, which changes the router-isolation story) or remove it from dependencies.

  • [minor] packages/api-client/src/subscriptions/run-events.ts:22maxEvents is part of the public RunEventSubscriptionOptions but subscribeRun ignores it. The destructure at line 96 omits it; only the private useRunEvents adapter slices. A consumer setting maxEvents on the public API gets no bound. Honor it in the subscription or move it to the hook's own options type.

  • [minor] packages/react/src/core/storage.ts:12-18 — the localStorage guard covers access but not the operations. localStorageWhenAvailable() try/catches reading globalThis.localStorage, then setItem is called unguarded. In the third-party embedding contexts this package exists for, setItem throws QuotaExceededError, and Safari/ITP and some partitioned-storage configurations throw SecurityError on the operation rather than on the property access. An uncaught throw from a storage write will take down the host's render. Wrap the three operations.

  • [minor] packages/api-client/src/domains/runs.ts:50runs.list validates the run list all-or-nothing, and version skew is now possible for the first time. getRuns previously went through unwrap, which casts without validating (packages/web/src/api/client.ts:306); it now goes through requestJson(apiRunSchema.array(), …). Validating is the right direction and matches CODE_REVIEW.md's "zod at every boundary" — the granularity is the issue. apiRunSchema is the fat runRecordSchema, and one record that fails to parse now rejects the entire array, so the task list errors out instead of rendering the other 200 runs. That was acceptable while the client only ever shipped in lockstep with its server; once this package is published and version-pinned, an older client against a newer server is a real combination, and BACKWARD_COMPATIBILITY.md §3/§9's established posture for exactly this is per-entry salvage ("a corrupt registry entry is dropped per-entry, never the whole array"). Consider parsing per element and dropping the unparseable ones.

  • [minor] packages/web/src/api/run-events.ts:36 — a fresh client is constructed on every effect run. createCezarClient({ baseUrl: getApiBaseUrl() }) inside the useEffect means every runId/option change allocates a new client and increments the module-level identity counter. Harmless while identity is unused here, but it compounds the finding above; hoisting it to a useMemo keyed on the base URL would be equivalent and cheaper.

  • [minor] .github/workflows/ci.yml:118 and scripts/release-snapshot.mjs:160 — the npm preview comment never names the package this PR exists to ship. The emitted result carries rootName, apiClientName and aliasName only, and the sticky PR comment renders "Packages: aliasrootapiClient" with install lines built from the alias alone. @open-mercato/cezar-contract and @open-mercato/cezar-react are published but invisible in the preview, so a reviewer cannot find the snapshot version of the facade. publishedNames is already computed — surface it.

  • [minor] Scope: the notifications-section.tsx save-race fix is unrelated to the facade. packages/web/src/routes/settings/notifications-section.tsx:38-62 adds a pendingEnabled guard plus cancelQueries to stop a stale server read from clobbering an optimistic toggle. It looks correct, but it is an independent bug fix inside a 105-file, 7,195-line PR, where it will not be found again by anyone reading the history for that bug.

📝 Nits

  • packages/react/src/core/provider.tsx:154-155 and packages/web/src/cockpit-implementation.tsx:54-55 assign onErrorRef.current = onError during render. It is a common idiom, but React's guidance is not to write refs while rendering; a layout effect or useEffectEvent says the same thing without the caveat.
  • packages/api-client/src/client.ts:216resolveProjectUrl classifies a protocol-relative //host/path as relative (the absolute test requires a scheme), so new URL resolves the host and the function then rebuilds the URL against baseUrl, silently dropping it. No caller produces one today.
  • packages/react/dist is 3.0 MB (2.8 MB of it assets/, including four .woff2 faces) and dist/styles.css is 134 KB. Worth stating in the README so an embedder knows the cost up front.

💥 Breaking-Changes Checklist

  • No CLI command, flag, alias, env var, or exit code changed (BACKWARD_COMPATIBILITY.md §1).
  • No /api/v1 route, response shape, or SSE event name changed; packages/web/src/api/run-events.ts moves the subscription mechanics into the api-client while preserving the wire contract — both event names, seq > maxSeq replay dedup, the afterSeq/cursor resume rule, the 40 s liveness watchdog, the CLOSED-only reopen, and the pagehide/pageshow/visibility handling all survive the move (packages/api-client/src/subscriptions/run-events.ts).
  • Zod validation at the boundary is preserved and in places tightened — getRunHistory/getRunHistoryContext keep their schemas via requestJson(schema, …), and runs.list now validates where unwrap previously did not.
  • No .ai/cezar/ or ~/.cezar/ state file shape changed.
  • Three packages become public npm surfaces with no entry in BACKWARD_COMPATIBILITY.md — the blocker above. Per that document's §6 the section is required before the surface exists, and per CODE_REVIEW.md this is a blocker rather than a follow-up.
  • Two of the new public exports paths resolve to empty modules, committing the project to entry points that do nothing (major above).

🧪 Test Coverage

Coverage of the new code is genuinely good and I want to be clear about that: cockpit.test.tsx covers memory-routing containment, controlled-path echo, search/hash preservation across redirects, owned-vs-supplied query-client lifetimes including the StrictMode replay, and the error-boundary fallback; provider.test.tsx covers root adoption and exact restoration, appearance isolation, the system-theme listener, scoped error reporting, and the link/button navigation fallback; scope-css.test.ts, verify-css.test.ts and styles-source.test.ts pin the stylesheet transform; the release change ships tests in both snapshot.test.ts and stable.test.ts; and cockpit-facade.e2e.ts proves the standalone app really goes through the facade with no iframes.

Three gaps, in order of consequence:

  1. The packed-consumer path has tests but no gate. scripts/check-cockpit-pack.test.mjs and the fixture exist; nothing runs them (major above). The strongest test in this PR is the one that never executes.
  2. No test covers two concurrent CezarCockpit instances. That is the documented limitation and the documented use case (the PR body's sandbox integration mounts one cockpit per workspace), so a test pinning the current single-owner behavior — even as an explicit "this is what happens today" assertion — would keep the boundary honest.
  3. The identity/storage tests assert against a literal fake. As noted above, fakeCezarClient('client-a') means the counter-derived identity is never exercised; a test constructing two real clients and checking their storage keys is what would have caught it.

@pat-lewczuk pat-lewczuk added feature New capability needs-qa Requires manual QA before merge labels Aug 28, 2026
@pat-lewczuk

pat-lewczuk commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr — 🏷️ label rationale

  • changes-requested — the re-review of a6fc6d31 found the source clean at major-and-above (every previous blocker and major is fixed), but the head no longer merges into main: main released 0.10.1 in chore(release): bump main to 0.10.1 and cut the changelog entry #962 while this branch is still on 0.10.0, so package-lock.json and packages/web/package.json conflict.
  • feature — the PR adds a new publishable package (@open-mercato/cezar-react) and makes the contract and API-client packages public, so hosts can embed the complete cockpit without an iframe.
  • 🧪 needs-qa — the change alters how the real cockpit mounts, routes, and styles itself, and no amount of reading the diff substitutes for clicking through an embedded instance; a QA reviewer still needs to add qa-approved before this can merge.
  • 🔴 priority-high — it is the gating dependency for sandbox hosts that need a native cockpit, and it is the first change to make this repository publish public npm libraries at all.
  • 🔴 risk-high — 105 files and ~7,200 lines, three new public npm surfaces with a fresh BACKWARD_COMPATIBILITY.md section, and changes to the CI, snapshot, nightly and stable release paths.

@pat-lewczuk pat-lewczuk removed their assignment Aug 28, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

Thanks @andrzejewsky — review found actionable items, so I'm handing this PR back to you for the next pass. When the updates are pushed, re-request review and the automation can pick it up from the latest head.

Two of the findings need a decision rather than a patch, so flagging them here:

  1. The npm scope permission is the gating one. Publish npm snapshot failing is not merge-blocking (main has no required status checks — only the 1-approval pull_request rule), but publish-snapshot runs on every same-repo PR and scripts/release-snapshot.mjs / scripts/release.mjs only degrade to a dry run when NPM_TOKEN is absent, never when it lacks scope rights. So merging as-is turns CI red for every future PR here and breaks npm run release. Either the org grant lands first, or both scripts learn to treat E404/E403 on a not-yet-created scoped package as the loud dry run they already have.
  2. Publishing the contract and api-client reverses a documented decision. docs/publishing.md previously recorded that the api-client stays private "until its surface stops moving" because it still carries the hand-written DTOs. This PR deletes that paragraph without arguing the condition is met, and BACKWARD_COMPATIBILITY.md §6 asks for a section before a library surface exists. If the intent is that these surfaces are provisional, saying so explicitly in the new section is enough — but it needs saying.

The facade code itself reads well and the test coverage is genuinely strong; most of the remaining items are small and mechanical (wiring test:cockpit-package and check:boundaries into CI, the .tsx typecheck glob, the two empty subpath exports, the dead styles/{base,tokens}.css).

@pat-lewczuk pat-lewczuk removed the in-progress Cezar agent is actively working this issue label Aug 28, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr completed: CHANGES REQUESTED. Lock released.

Reviewed ff81d86a in an isolated worktree. The release-set defect from the previous pass is confirmed fixed — @open-mercato/cezar-react is stamped, dependency-pinned and published in both channels, with tests in snapshot.test.ts and stable.test.ts. Two blockers and six majors remain, detailed in the review.

Local validation: typecheck ✅, test:unit ✅ (36), build ✅ (including scope-css, verify-css, verify-cockpit-artifact, check:pack), test:package ✅ (15). npm test failed non-deterministically in my sandbox only (8 then 12 failures across identical runs, no failing file in this diff) because os.tmpdir() here sits inside the repository, so the "outside a git repo" degradation tests find one; CI is green on this head. I also ran the two gates this PR ships but never wires up — npm run test:cockpit-package passes (3 tarballs installed, 1 typecheck, 1 Vite build, 4 fonts resolved, 70 runtime/declaration files scanned) and check:boundaries exits 0 — which is the point: they are correct today and nothing runs them.

autofix: skipped (not my PR — re-run with --autofix to fix it here).

@andrzejewsky

Copy link
Copy Markdown
Contributor Author

Addressed the Aug 28 review in 6df5f0ed.

Delivery/public surface:

  • CI now runs the cold packed-consumer gate, applies the React import-boundary scan through build, and dry-packs all five public release packages.
  • Preview, nightly, and stable release summaries use publishedNames, so contract and React are visible; stable bump PRs now include all five manifests.
  • Added the public-library compatibility contract, including provisional pre-1.0 rules, stable client identity, React/CSS hooks, and the coarse cockpit boundary.
  • Removed the empty ./tasks and ./session exports/files, dead CSS files, and the unnecessary runtime react-router install; TSX tests now participate in typecheck; README documents the roughly 3 MB uncompressed cockpit cost.

Runtime correctness:

  • Client identity is stable by normalized base URL + credential/auth mode, with explicit identity isolation for same-authority logical installations.
  • Legacy transport mutation moved to a layout effect and warns on concurrent cockpit ownership.
  • Browser storage operations degrade safely, run lists salvage valid entries, protocol-relative authorities survive URL projection, callback refs update after commit, and maxEvents now belongs only to the accumulating React adapter.
  • The web SSE adapter reuses a cached client while resolving the legacy authority after the parent layout-effect lease.

Two review points were adjudicated rather than changed blindly:

  • npm E403/E404 remains fail-loud when a token is present. The failing snapshot needs scope-wide publish/create rights for @open-mercato; treating an invalid credential as a dry run would mask a real release failure. Missing tokens still degrade loudly to dry run as before.
  • I retained the notification save-race guard after proving that removing it makes the existing isolated notification contract test fail deterministically (aria-checked is reset to false by the stale GET). The guard is required to keep the suite green, not incidental cleanup.

Validation:

  • root typecheck: pass
  • changed/public surface: 184 tests pass
  • unit: 35 pass, 1 platform skip
  • build + boundary/CSS/artifact/tarball gates: pass
  • packaged CLI E2E: 15 pass
  • cold tarball consumer: 12 gate tests pass; 3 tarballs install, consumer typechecks/builds, 4 fonts resolve, 66 shipped runtime/declaration files scan clean
  • all 5 release packages dry-pack successfully

The full suite reaches 6,278/6,287 passing locally; the remaining 9 are the same nondeterministic temp-directory teardown races in the three untouched server suites called out in the review (run-isolation, run-lease, workspace-semaphore). No changed-file suite fails.

@andrzejewsky

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in 03272da4: snapshot-release fixtures now clear the ambient GITHUB_RUN_ATTEMPT unless a test explicitly supplies it. This fixes the rerun-only .2 version suffix leak. Verified with GITHUB_RUN_ATTEMPT=2 npm run test:package: 15/15 packaged CLI E2E tests pass.

@andrzejewsky

Copy link
Copy Markdown
Contributor Author

CI follow-up: the complete code/package gate is green (typecheck, unit + server/cockpit tests, build, cold installed consumer, packaged CLI E2E, and all release tarballs). The separate snapshot job reaches npm and then fails on the first new package with E404 for @open-mercato/cezar-contract: the workflow token does not have permission to create/publish packages in the @open-mercato npm scope. This is now the only remaining check failure and requires npm org/token permission; the workflow intentionally fails loudly.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

📦 npm preview published — 0.10.0-pr931.1376.2

Try this PR build (exact pinned version — copy-paste as-is):

npx cezar-cli@0.10.0-pr931.1376.2                                # cockpit at http://localhost:4321
npx cezar-cli@0.10.0-pr931.1376.2 run "…"                        # headless run
npx cezar-cli@0.10.0-pr931.1376.2 server-deploy --platform <id>  # roll a server to this exact build

Also tagged: npm install -g cezar-cli@pr-931 (moving tag for this PR).
Packages: @open-mercato/cezar-contract@0.10.0-pr931.1376.2, @open-mercato/cezar-api-client@0.10.0-pr931.1376.2, @open-mercato/cezar-react@0.10.0-pr931.1376.2, @open-mercato/cezar@0.10.0-pr931.1376.2, cezar-cli@0.10.0-pr931.1376.2 (provenance attested).

@pat-lewczuk pat-lewczuk self-assigned this Sep 10, 2026
@pat-lewczuk pat-lewczuk added the in-progress Cezar agent is actively working this issue label Sep 10, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr started by @pat-lewczuk at 2026-09-10T19:26:22Z. Other auto-skills will skip this PR until the lock is released.

Note: the only pre-existing claim signal was the author assignee left by this skill's own 2026-08-28 changes-requested handoff (that run posted Lock released. and no in-progress label remained). This is a re-review of the new head a6fc6d31, not an override of a live lock.

@pat-lewczuk pat-lewczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Re-review: feat(react): publish native Cezar cockpit facade

Reviewed a6fc6d31 in an isolated worktree (npm ci from the lockfile), against the Aug 28 pass on ff81d86a.

🎯 Summary

Every finding from the previous review is fixed, and the source-level review is now clean at major-and-above. Both blockers are cleared, all six majors are cleared, all seven minors and all three nits are cleared, and the three test-coverage gaps I named are closed by tests that target exactly the properties that were unpinned. The fixes are not cosmetic — the client identity is genuinely derived, the packed-consumer gate genuinely runs in CI, and the compatibility document genuinely describes the surface being frozen.

What now blocks the merge is not the code: main released 0.10.1 (#962) while this branch sat at 0.10.0, so the head no longer merges. That is the one blocker, and resolving it is more than a two-file fixup because this PR turns the release set into five lockstep-versioned packages.

🧭 Verdict

Request changes — one blocker, the unresolved conflict with main. No majors. Four minors, none of which need to hold the PR if you would rather take them as follow-ups; I would fix the first one in this pass because it undermines the diagnostic the previous review asked for.

⛔ Blocker

  • [blocker] The head conflicts with main and cannot merge (mergeable: CONFLICTING, mergeStateStatus: DIRTY). Conflicting paths: package-lock.json and packages/web/package.json.

    The cause is a version-bump collision, not a code collision: e8c95f3a chore(release): bump main to 0.10.1 and cut the changelog entry (#962) moved main's whole release set to 0.10.1, while this branch still carries 0.10.0 in all six manifests (packages/contract, packages/api-client, packages/react, packages/cezar, packages/web, alias-cezar) with ^0.10.0 intra-release pins.

    Please merge main forward rather than resolving only the two conflicted files. Taking main's side on packages/web/package.json would drop the new "@open-mercato/cezar-react" dependency this PR adds; taking this branch's side would roll the version back to 0.10.0. The correct resolution stamps all five release manifests plus alias-cezar to 0.10.1 and rewrites every intra-release pin (cezar-contract ← api-client, cezar-api-client ← react and web, cezar-react ← web, cezar ← alias) to ^0.10.1, then regenerates package-lock.json with npm install. packages/contract and packages/react are new in this PR and have never been through a bump, so they are the two most likely to be missed.

    Caveat, stated plainly: I reviewed the diff as pushed. The forward-merge touches manifests and the lockfile, so the release-order tests in packages/cezar/test/e2e/release-snapshot.test.ts and stable.test.ts are worth a second look once it lands.

✅ Previous findings — disposition

Both blockers resolved:

  • npm scope permission. Cleared by the route I flagged as the alternative to code changes: the @open-mercato scope grant has landed. Publish npm snapshot is green on this exact head (run 33427089501, job 99962821679) and actually published 0.10.0-pr931.1376.2, so the E404 on @open-mercato/cezar-contract is gone and the stable npm run release path is unblocked. See minor 3 for the residue.
  • BACKWARD_COMPATIBILITY.md. §6b is added and does the job properly — it names the frozen exports per package, the CezarProvider/CezarCockpit prop shapes, the CezarCockpitRouting modes, the stylesheet entry, the .cezar-root class and all six data-cezar-* hooks, states what is not contract (hashed chunks, Tailwind classes, markup below the root), gives the pre-1.0 and post-1.0 change paths, and pins npm run test:cockpit-package as a gate that must stay in CI. §6a's stale "there is no exports/library API" claim is corrected accurately (./app-type really is the service package's only library export; . is the shebang CLI entry). docs/publishing.md now argues the public set coherently instead of just asserting it.

All six majors resolved, each verified rather than taken on trust:

  • test:cockpit-package now runs in ci.yml, nightly.yml and release.yml.
  • check:boundaries is the first leg of packages/react's build, which the root build reaches via build:react — so it runs on every build, not just in CI.
  • "Verify release packages" packs all five publishable workspaces, and the misleading comment is corrected.
  • ./tasks and ./session are gone from exports, vite.config.ts and the source tree — and assertReactTarball now fails if they reappear, which is the better half of the fix.
  • useLegacyTransportBaseUrl moved into useLayoutEffect with a dev warning on lease contention.
  • CezarClient.identity is derived from the normalized authority + credential mode + auth mode, with an explicit identity escape hatch, documented in both §6b and the README.

Minors and nits: tsconfig.test.json now includes .tsx; the dead base.css/tokens.css are deleted; react-router moved to devDependencies; maxEvents moved out of the public RunEventSubscriptionOptions into the web adapter that actually implements it; browser storage wraps all three operations; runs.list salvages per entry; the cockpit's run-events client is cached per authority; publishedNames now drives the preview comment, the nightly summary and the release table; refs are assigned in a layout effect; protocol-relative URLs survive resolveProjectUrl; the README states the ~3 MB cost.

🔽 Minors

  • [minor] packages/react/src/cockpit.tsx:49-58 — the new lease-contention warning false-fires on the remount pattern this PR documents. The cleanup defers its reset into queueMicrotask, but React runs the outgoing tree's layout-effect destroy and the incoming tree's layout-effect create in the same commit. So on a keyed remount — key={JSON.stringify([sandboxId, origin])}, exactly what the PR description and packages/react/README.md tell hosts to do when switching sandboxes — the new instance's effect still sees the old instance's symbol in activeLegacyTransportOwner and warns.

    Reproduced in the review worktree with a scratch test: render <CezarCockpit key="sandbox-a" …>, wait for getApiBaseUrl() to be https://first.example.test, rerender with key="sandbox-b", wait for https://second.example.test — the base URL is correct throughout, and console.warn receives "cezar: multiple CezarCockpit instances share a legacy transport; only the most recently mounted authority can be active" with exactly one cockpit mounted.

    Behavior is fine; the diagnostic is what breaks. The previous review asked for this warning so that two genuinely concurrent cockpits would be diagnosable instead of presenting as B fetching A's data. A warning that also fires on the documented single-instance happy path trains developers to ignore it, which costs the fix its value. Track live owners in a Set (add on create, delete synchronously in cleanup, warn when size > 1), or reset activeLegacyTransportOwner synchronously in cleanup and keep queueMicrotask only for the setApiBaseUrl(''). A test asserting silence on keyed remount would pin it.

  • [minor] packages/react/src/cockpit.tsx:48 — moving the lease to a layout effect drops a render-time guarantee that one consumer still relies on. The comment previously read "Install the public client's authority synchronously so private render-time URL resolution sees it"; it now reads "before paint". That is a real weakening: during the embedded cockpit's first render pass, getApiBaseUrl() is still '', and packages/web/src/components/zoomable-image.tsx:32 calls resolveApiUrl(rawSrc) during render (packages/api-client/src/utils/project-scope.ts:144 reads the module base). A URL resolved in that window would be root-relative — pointing at the host's origin rather than the Cezar origin.

    I could not construct a failure on this tree, and I want to be precise about why: both ZoomableImage call sites (routes/task-thread/thread-items.tsx:200,643 and components/diff/image-preview.tsx:49) are gated behind fetched data, so neither can render before the layout effect has run. The finding is that nothing pins that — the next render-time consumer of resolveApiUrl/getApiBaseUrl that is not data-gated will silently resolve against the wrong origin, and no test will notice. Either restore the render-phase install for the read path only, or state the "no render-time reads of the module base" invariant where setApiBaseUrl is defined and give check:boundaries something to enforce.

  • [minor] scripts/release-snapshot.mjs:113 and scripts/release.mjs:99 still degrade only on a missing NPM_TOKEN, never on a token that lacks scope rights. Now that the scope grant has landed this is no longer blocking — but it is the same cliff, one credential rotation away. Both scripts hard-fail the job on an E403/E404 from npm publish, which is what turned every PR in this repository red for five days. Treating a permissions error on a scoped package as the same loud dry run they already have for a missing token would make that failure mode self-describing instead of a red badge on unrelated PRs. Reasonable as a follow-up issue rather than a change here.

  • [minor] Scope: the notifications-section.tsx save-race fix is still bundled. packages/web/src/routes/settings/notifications-section.tsx adds a pendingEnabled guard plus cancelQueries so a stale server read cannot clobber an optimistic toggle. It still looks correct and it is still an independent bug fix inside a 105-file, 7,200-line PR, where nobody reading the history for that bug will find it. Carried forward unchanged from the last pass — not worth a rebase now, just noting it stays true.

🧪 Validation Gate

Run in an isolated worktree at a6fc6d31, npm ci restored from the lockfile. All five commands pass.

Command Result Evidence
npm run typecheck ✅ Pass Contract, API client, React, service and web all clean — and the React leg now genuinely covers .tsx tests after the tsconfig.test.json fix.
npm test ✅ Pass 343 files, 6324/6324 tests. First run showed 7 failures; all 7 were the sandbox artifact I recorded last time — os.tmpdir() resolves inside the repository here, so every test asserting "outside a git repository" finds one by walking up. Re-run with TMPDIR=/tmp/…: 0 failures. None of the 7 files appear in this diff. Recording the diagnosis, not a finding.
npm run test:unit ✅ Pass 36 passed.
npm run build ✅ Pass All five workspaces. check:boundaries now runs first in the React leg (exit 0); scope-css, verify-css, verify-cockpit-artifact and check:pack all pass.
npm run test:package ✅ Pass 16 passed, up from 15 — including the rerun-attempt isolation added in 03272da4.

CI on this head is fully green as well: Unit, build, E2E, and package, Publish npm snapshot, and license/cla all SUCCESS. No required check is failing and none is pending. main has no required status checks configured, so the merge button is gated by the review requirement, not by CI.

💥 Breaking-Changes Checklist

  • No CLI command, flag, alias, env var or exit code changed (§1).
  • No /api/v1 route, response shape or SSE event name changed; the run-event subscription move preserves the wire contract.
  • Zod validation at the boundary preserved, and runs.list now salvages per entry in line with §3/§9's established posture.
  • No .ai/cezar/ or ~/.cezar/ state file shape changed.
  • Three packages become public npm surfaces — now recorded in BACKWARD_COMPATIBILITY.md §6b before the surface ships, which is the path §6 requires. Previously the second blocker; resolved.
  • No public exports path resolves to an empty module./tasks and ./session removed, and the pack gate now fails if they return. Previously a major; resolved.

Two removals worth naming explicitly, both safe: ./tasks/./session and RunEventSubscriptionOptions.maxEvents were removed from packages that were private on main and have only ever been published under per-PR snapshot tags. No stable release advertised them, so no deprecation window is owed.

🧪 Test Coverage

All three gaps from the previous pass are closed, and closed at the right level:

  1. The packed-consumer path now has a gate, in ci.yml, nightly.yml and release.yml — the strongest test in this PR now actually executes.
  2. Two concurrent cockpits are covered (cockpit.test.tsx, "warns when a second cockpit contends for the legacy transport lease") — the documented limitation is pinned by an assertion instead of by prose. What it does not yet pin is the absence of that warning for one cockpit; see minor 1.
  3. Identity is exercised against real clients (client.test.ts, "derives a stable identity from the normalized authority and credential mode") — including trailing-slash normalization and credential-mode divergence, which is what the old fake could never have caught.

New tests are well targeted throughout: per-entry run salvage against a genuinely malformed row, storage degradation against a localStorage that throws SecurityError/QuotaExceededError on the operations rather than on property access, protocol-relative authority preservation, and a pack-gate assertion that the removed entries stay removed.

@pat-lewczuk pat-lewczuk removed their assignment Sep 10, 2026
@pat-lewczuk

pat-lewczuk commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

@andrzejewsky — this one is close. Every finding from the Aug 28 review is fixed, and I verified each rather than taking the commit message for it: the packed-consumer gate and the import-boundary scan really do run now, the derived client identity really is stable across trailing-slash normalization, BACKWARD_COMPATIBILITY.md §6b really does freeze the surface it needs to, and the full local gate is green (6324/6324 tests) as is CI on this head — the npm scope grant landed, so Publish npm snapshot is passing.

The one blocker is the forward-merge, and it needs a little care: main cut 0.10.1 in #962, so package-lock.json and packages/web/package.json conflict. Please merge main in rather than resolving just those two files — taking main's side on packages/web/package.json silently drops the @open-mercato/cezar-react dependency this PR adds, and taking yours rolls the version back. All five release manifests plus alias-cezar need stamping to 0.10.1 with their intra-release pins rewritten to ^0.10.1; packages/contract and packages/react are new here and have never been through a bump, so they are the easy ones to miss. Worth re-running npm run test:package after, since the release-order tests read those manifests.

One thing I would fix in the same pass — minor 1 in the review: the new lease-contention warning fires on a single cockpit remounted by key, which is the sandbox-switching pattern your own PR description and README recommend. I reproduced it in the review worktree; behavior is correct, but a warning that cries wolf on the happy path is one developers learn to ignore, and it exists precisely to make the real two-instance bug diagnosable.

The other three minors are genuinely optional and would be fine as follow-up issues. Push the update and re-request review.

@pat-lewczuk pat-lewczuk removed the in-progress Cezar agent is actively working this issue label Sep 10, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr completed: CHANGES REQUESTED. Lock released.

Re-reviewed a6fc6d31 in an isolated worktree. Every finding from the 2026-08-28 pass is fixed and verified — both blockers, all six majors, all minors and nits, and the three test-coverage gaps. The npm scope grant landed, so Publish npm snapshot is green; CI is fully green on this head and the local gate passes all five commands (typecheck, 6324/6324 tests, unit, build, package).

One blocker remains: the head conflicts with main after the 0.10.1 release bump (#962). Four minors are listed in the review; none of them need to hold the merge.

autofix: skipped (not my PR — re-run with --autofix to fix it here).
CI follow-up: none needed — all checks are settled and green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Reviewer requested changes feature New capability needs-qa Requires manual QA before merge priority-high Release-blocking risk-high Wide blast radius, review deeply

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants