From b02886c96174f775fd096e9221de531a1c97fd99 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 14:37:57 +0000 Subject: [PATCH 1/2] fix(cli): refuse to serve a drifted vendored console under `os dev` (#7752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/console/dist` is a gitignored local build that only `scripts/build-console.sh` (`pnpm objectui:build`) refreshes — `turbo run build` never touches it. Pull a branch that moves `.objectui-sha` and the pin advances while the dist stays frozen, so the server keeps serving a Console SPA the repo no longer pins. `pnpm check:console-sha` already fails on exactly this, but it is wired into the root `pnpm dev` / `dev:showcase` / `dev:crm` / `dev:todo` scripts only. Every other way to boot reaches the server without passing it: `objectstack dev` run inside an example dir, an example's own `dev` script (`objectstack dev --seed-admin`), a `.claude/launch.json` config driving `pnpm exec objectstack dev` — which is the path the QA runner takes. A sweep booted that way and spent its run measuring a console two days behind the pin; two of its clauses had to be recorded `blocked` once the gap was found. So the guard gets a second seat, on the boot path itself. Resolution now reports proven drift to its caller instead of warning about it in place, and `decideConsoleMount` turns that into a decision: under `os dev` the Console does not mount at all, and the boot prints the rebuild remediation. The stale bundle becomes unreachable rather than silently authoritative — `/_console/` 404s and the banner advertises no console URL — while the API keeps serving, so api/cli work is unaffected. Deliberately narrow: `isDev` only, and only on drift it can prove. A published install ships no `.objectui-sha` pin and the sibling-repo dev fallback writes no stamp, so both keep resolving exactly as before and no production or cloud deployment can reach the refusal. `OS_ALLOW_CONSOLE_DRIFT=1` boots the stale bundle deliberately. The runner-facing docs say the same thing from the other side: a 404 `/_console/` after this change reads "rebuild with `pnpm objectui:build`", never "console broken". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LkvcB2Ei8Mpaa87N2US5wY --- .changeset/console-sha-drift-dev-guard.md | 33 +++++ .claude/skills/dogfood-verification/SKILL.md | 9 ++ docs/qa/platform-checklist/RUNNER.md | 2 +- packages/cli/src/commands/serve.ts | 28 +++- .../cli/src/utils/console.sha-drift.test.ts | 113 +++++++++++++++ packages/cli/src/utils/console.ts | 130 +++++++++++++++--- packages/cli/test/console-resolve.test.ts | 23 ++-- 7 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 .changeset/console-sha-drift-dev-guard.md create mode 100644 packages/cli/src/utils/console.sha-drift.test.ts diff --git a/.changeset/console-sha-drift-dev-guard.md b/.changeset/console-sha-drift-dev-guard.md new file mode 100644 index 0000000000..425e5ce34c --- /dev/null +++ b/.changeset/console-sha-drift-dev-guard.md @@ -0,0 +1,33 @@ +--- +"@objectstack/cli": patch +--- + +fix(cli): `os dev` refuses to serve a console built from a different objectui SHA than the pin (#7752) + +`packages/console/dist` is a gitignored local build that only +`scripts/build-console.sh` (`pnpm objectui:build`) refreshes — `turbo run +build` never touches it. Pull a branch that moves `.objectui-sha` and the pin +advances while the dist stays frozen, so the server keeps serving a Console +SPA the repo no longer pins. + +`pnpm check:console-sha` already fails on exactly this, but it is wired into +the root `pnpm dev` / `dev:showcase` / `dev:crm` / `dev:todo` scripts only. +Every other way to boot reaches the server without passing it — `objectstack +dev` run inside an example dir, an example's own `dev` script (`objectstack +dev --seed-admin`), a `.claude/launch.json` config driving `pnpm exec +objectstack dev`. A QA sweep booted that way and spent its run measuring a +console two days behind the pin; two of its clauses had to be recorded +`blocked` once the gap was found. + +So the guard gets a second seat on the boot path itself. When the dist carries +an objectui stamp that provably differs from the repo's pin, `os dev` now +declines to mount `/_console` and prints the rebuild remediation, instead of +serving the stale bundle under a warning that scrolls past. The API still +boots, so api/cli work is unaffected, and the banner stops advertising a +console URL that would have been a lie. + +Deliberately narrow: dev only, and only on drift it can prove. A published +install ships no `.objectui-sha` pin and the sibling-repo dev fallback writes +no stamp — both keep resolving exactly as before, and no production or cloud +deployment can reach the refusal. `OS_ALLOW_CONSOLE_DRIFT=1` boots the stale +bundle anyway when that is what you want. diff --git a/.claude/skills/dogfood-verification/SKILL.md b/.claude/skills/dogfood-verification/SKILL.md index 712bb5951f..966c99f969 100644 --- a/.claude/skills/dogfood-verification/SKILL.md +++ b/.claude/skills/dogfood-verification/SKILL.md @@ -74,6 +74,15 @@ unsaved drafts that block navigation, and dirty the working tree. Isolate up fro (separate origin → needs its own `admin@objectos.ai/admin123` login). Skipping this cost a full spawn-a-fix cycle on an action-create dead-end that was already fixed in objectui main. +- [ ] **A drifted build no longer boots silently (#7752).** `build-console.sh` stamps the + SHA it built from into `packages/console/dist/.objectui-sha`; when that stamp differs + from the repo's `.objectui-sha` pin, `os dev` **refuses to mount `/_console`** and + prints the remediation — `/_console/` 404s and the banner advertises no console URL. + That is a precondition to fix, not a product bug: run **`pnpm objectui:build`** + (rebuild at the *pinned* SHA — `objectui:refresh` would re-bump the pin to your local + `../objectui` HEAD) and reboot. `pnpm check:console-sha` reports the same comparison + without booting. `OS_ALLOW_CONSOLE_DRIFT=1` boots the stale bundle deliberately — and + then every console observation you make describes a commit the repo does not pin. ## 3. Verify — visual / API first, DOM last (the anti-false-positive rule) diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index 1894fd6bc0..a24378e360 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -101,7 +101,7 @@ contradicts it, and correct it here when it does. | trap | what it fakes | counter | |---|---|---| | `hydration-race` | empty nav/list right after navigation | screenshot first; settle; then read DOM | -| `stale-console-bundle` | UI bug already fixed upstream in objectui | check against objectui HMR console / fresh build (skill §2) | +| `stale-console-bundle` | UI bug already fixed upstream in objectui | check against objectui HMR console / fresh build (skill §2). `os dev` refuses to mount a console whose stamp ≠ the `.objectui-sha` pin, so a 404 `/_console/` reads "rebuild with `pnpm objectui:build`", never "console broken" | | `stale-dist` | src edits with no runtime effect | rebuild package + restart before judging | | `automation-input` | dead buttons / empty submits caused by the driver | ref-targeted clicks; native setter + input/change events | | `shared-browser-tab` | drifting origin, foreign drafts | pin absolute origin; own port/DB (skill §0) | diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index bb6f575c6b..e7a8aa8b77 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -71,8 +71,12 @@ import { CONSOLE_PATH, resolveConsolePath, hasConsoleDist, + decideConsoleMount, + formatConsoleShaDriftWarning, + formatConsoleShaDriftRefusal, createConsoleStaticPlugin, createRuntimeAssetsPlugin, + type ConsoleShaDrift, } from '../utils/console.js'; import dotenvFlow from 'dotenv-flow'; @@ -2630,20 +2634,38 @@ export default class Serve extends Command { // opt out of the Console entirely — useful for control-plane // deployments where the runtime Console is meaningless. const consoleEnabled = flags.console && process.env.OS_DISABLE_CONSOLE !== '1'; - const consolePath = consoleEnabled ? resolveConsolePath() : null; - const consoleWillMount = !!(consolePath && hasConsoleDist(consolePath)); + // Resolution reports objectui-SHA drift instead of warning about it + // itself, so the decision below owns the single message about it. + // (Boxed so the assignment made inside `onDrift` is visible to + // control-flow analysis after the call returns.) + const driftBox: { value: ConsoleShaDrift | null } = { value: null }; + const consolePath = consoleEnabled + ? resolveConsolePath({ onDrift: (d) => { driftBox.value = d; } }) + : null; + const consoleDrift = driftBox.value; + const { mount: consoleWillMount, refusedForDrift } = decideConsoleMount({ + hasDist: !!(consolePath && hasConsoleDist(consolePath)), + drift: consoleDrift, + isDev, + }); // ── Console portal ────────────────────────────────────────── // The opinionated, fork-ready runtime console (`@object-ui/console`, // published from the objectstack-ai/objectui monorepo) mounts under // `/_console/`. When present, it owns the root `/` redirect // (preferred default UI). It is optional — we only mount it when - // the package resolves and a pre-built `dist/` is present. + // the package resolves and a pre-built `dist/` is present, and — in + // dev — only when that build matches the repo's objectui pin (#7752). if (consolePath) { if (consoleWillMount) { + if (consoleDrift) { + console.warn(chalk.yellow(formatConsoleShaDriftWarning(consoleDrift))); + } const consoleDistPath = path.join(consolePath, 'dist'); await kernel.use(createConsoleStaticPlugin(consoleDistPath, { isDev })); trackPlugin('ConsoleUI'); + } else if (refusedForDrift && consoleDrift) { + console.error(chalk.red(formatConsoleShaDriftRefusal(consoleDrift))); } else { console.warn(chalk.yellow(` ⚠ Console dist not found — install \`@object-ui/console\` (already built) or run \`pnpm --filter @object-ui/console build\` in the objectui workspace`)); } diff --git a/packages/cli/src/utils/console.sha-drift.test.ts b/packages/cli/src/utils/console.sha-drift.test.ts new file mode 100644 index 0000000000..f518619541 --- /dev/null +++ b/packages/cli/src/utils/console.sha-drift.test.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectui-SHA drift guard — the seat on the boot path itself (#7752). + * + * `pnpm check:console-sha` guards the root `pnpm dev` / `dev:showcase` / + * `dev:crm` / `dev:todo` scripts. Every other way to boot reaches the server + * without passing it: `objectstack dev` run inside an example dir, an + * example's own `dev` script (`objectstack dev --seed-admin`), a + * `.claude/launch.json` config driving `pnpm exec objectstack dev`. A QA sweep + * booted that way and spent the run measuring a console two days behind the + * repo's pin, with a warning the boot log scrolled past. + * + * `decideConsoleMount` is what closes it: on drift it can *prove*, the dev + * server does not mount the Console at all, so the stale bundle is + * unreachable rather than silently authoritative. Detection semantics + * (`detectConsoleShaDrift`) are pinned in `test/console-resolve.test.ts`. + */ +import { describe, it, expect } from 'vitest'; +import { + decideConsoleMount, + formatConsoleShaDriftRefusal, + formatConsoleShaDriftWarning, + DRIFT_OVERRIDE_ENV, +} from './console.js'; + +/** The exact gap #7752 measured: pin two days ahead of the served bundle. */ +const drift = { + pin: '6314e87f2d49b1ff3b158c296f1b2a52d14dff68', + stamp: '09987b680aa1c3e4f5061d2b7c8a9e0f1a2b3c4d', + pinFile: '/repo/.objectui-sha', +}; + +describe('decideConsoleMount', () => { + it('refuses to mount a drifted console under `os dev`', () => { + expect(decideConsoleMount({ hasDist: true, drift, isDev: true, env: {} })).toEqual({ + mount: false, + refusedForDrift: true, + }); + }); + + it('mounts when the dist matches the pin — no false positive', () => { + expect(decideConsoleMount({ hasDist: true, drift: null, isDev: true, env: {} })).toEqual({ + mount: true, + refusedForDrift: false, + }); + }); + + it('leaves non-dev serves advisory — a published install carries no pin anyway', () => { + expect(decideConsoleMount({ hasDist: true, drift, isDev: false, env: {} })).toEqual({ + mount: true, + refusedForDrift: false, + }); + }); + + it(`boots the stale bundle deliberately when ${DRIFT_OVERRIDE_ENV} is set`, () => { + for (const value of ['1', 'true', 'yes', 'on', 'TRUE']) { + expect( + decideConsoleMount({ + hasDist: true, + drift, + isDev: true, + env: { [DRIFT_OVERRIDE_ENV]: value }, + }), + ).toEqual({ mount: true, refusedForDrift: false }); + } + }); + + it('does not read an unset-looking value as an override', () => { + for (const value of ['0', 'false', 'no', '']) { + expect( + decideConsoleMount({ + hasDist: true, + drift, + isDev: true, + env: { [DRIFT_OVERRIDE_ENV]: value }, + }), + ).toEqual({ mount: false, refusedForDrift: true }); + } + }); + + it('keeps "no dist at all" distinct from "refused for drift"', () => { + // Both leave the Console unmounted, but they need different messages — + // "run objectui:build" vs the existing "console dist not found" hint. + expect(decideConsoleMount({ hasDist: false, drift: null, isDev: true, env: {} })).toEqual({ + mount: false, + refusedForDrift: false, + }); + }); +}); + +describe('drift messages', () => { + it('names the rebuild, never the pin bump — refresh would move the pin instead', () => { + for (const message of [ + formatConsoleShaDriftWarning(drift), + formatConsoleShaDriftRefusal(drift), + ]) { + expect(message).toContain('pnpm objectui:build'); + } + // `objectui:refresh` appears only as the explicitly-labelled wrong turn. + expect(formatConsoleShaDriftRefusal(drift)).toContain( + "(Use 'pnpm objectui:refresh' only when you intend to move the pin", + ); + }); + + it('shows both SHAs and the escape hatch, so the boot log is self-explaining', () => { + const message = formatConsoleShaDriftRefusal(drift); + expect(message).toContain(drift.pin.slice(0, 12)); + expect(message).toContain(drift.stamp.slice(0, 12)); + expect(message).toContain(drift.pinFile); + expect(message).toContain(DRIFT_OVERRIDE_ENV); + }); +}); diff --git a/packages/cli/src/utils/console.ts b/packages/cli/src/utils/console.ts index c4a630e6c0..ec316754b1 100644 --- a/packages/cli/src/utils/console.ts +++ b/packages/cli/src/utils/console.ts @@ -146,12 +146,23 @@ export interface ResolveConsoleOptions { cliVersion?: string; /** Warning sink; defaults to `console.warn`. */ warn?: (message: string) => void; + /** + * Called when the dist about to be served was built from a different + * objectui SHA than the repo pins. Defaults to emitting the advisory + * one-liner on `warn`. A caller that enforces the pin itself — the dev + * server refuses to mount a drifted console, see `decideConsoleMount` — + * passes a collector instead, so the drift is reported exactly once and + * by whoever decides what to do about it. + */ + onDrift?: (drift: ConsoleShaDrift) => void; } export function resolveConsolePath(options?: ResolveConsoleOptions): string | null { const cwd = options?.cwd ?? process.cwd(); const cliVersion = options?.cliVersion ?? getCliVersion(); const warn = options?.warn ?? ((message: string) => console.warn(message)); + const onDrift = + options?.onDrift ?? ((drift: ConsoleShaDrift) => warn(formatConsoleShaDriftWarning(drift))); /** Version guard for vendored-package candidates (strategies 1 & 2). */ const versionOk = (dir: string, candidateVersion: unknown): boolean => { @@ -241,10 +252,12 @@ export function resolveConsolePath(options?: ResolveConsoleOptions): string | nu // Pass 1: prefer a candidate that actually has a built dist. for (const dir of candidates) { if (hasConsoleDist(dir)) { - // Loudly flag (but still serve) a dist built from a different objectui - // SHA than the framework pins — the silent-drift case the npm-major - // guard above can't see. No-op for published installs / unstamped dists. - warnOnConsoleShaDrift(dir, warn); + // Report a dist built from a different objectui SHA than the framework + // pins — the silent-drift case the npm-major guard above can't see. + // No-op for published installs / unstamped dists. What happens next is + // the caller's call: advisory by default, fail-closed under `os dev`. + const drift = detectConsoleShaDrift(dir); + if (drift) onDrift(drift); return dir; } } @@ -300,32 +313,115 @@ function findObjectuiPin(startDir: string): { pin: string; file: string } | null return null; } -export function warnOnConsoleShaDrift( - consoleDir: string, - warn: (message: string) => void, -): void { +/** A dist proven to be built from a different objectui commit than the pin. */ +export interface ConsoleShaDrift { + /** objectui SHA the dist was built from (`dist/.objectui-sha`). */ + stamp: string; + /** objectui SHA the repo pins (`/.objectui-sha`). */ + pin: string; + /** Absolute path of the pin file the comparison used. */ + pinFile: string; +} + +/** + * Compare a resolved console package's dist stamp against the repo pin. + * Returns `null` for every unprovable case — no monorepo pin (published + * install), no stamp (pre-guard build or the sibling-repo dev fallback), + * unreadable files — and only reports drift it can prove. + */ +export function detectConsoleShaDrift(consoleDir: string): ConsoleShaDrift | null { const found = findObjectuiPin(consoleDir); - if (!found) return; // published install / no monorepo pin — nothing to compare + if (!found) return null; // published install / no monorepo pin — nothing to compare const stampFile = path.join(consoleDir, 'dist', '.objectui-sha'); let stamp: string | null = null; try { if (fs.existsSync(stampFile)) stamp = fs.readFileSync(stampFile, 'utf-8').trim(); } catch { - return; // unreadable stamp — fail open + return null; // unreadable stamp — fail open } - // Unstamped dist (pre-guard build or sibling-repo fallback): can't prove - // drift; `pnpm check:console-sha` surfaces it — don't nag on every boot. - if (!stamp || stamp === found.pin) return; + // Unstamped dist: can't prove drift; `pnpm check:console-sha` surfaces it. + if (!stamp || stamp === found.pin) return null; + + return { stamp, pin: found.pin, pinFile: found.file }; +} - warn( - ` ⚠ Console version drift: serving @objectstack/console built from objectui@${stamp.slice(0, 12)}, ` + - `but ${found.file} pins objectui@${found.pin.slice(0, 12)}. ` + +/** Advisory one-liner — used where the drifted console is still served. */ +export function formatConsoleShaDriftWarning(drift: ConsoleShaDrift): string { + return ( + ` ⚠ Console version drift: serving @objectstack/console built from objectui@${drift.stamp.slice(0, 12)}, ` + + `but ${drift.pinFile} pins objectui@${drift.pin.slice(0, 12)}. ` + `packages/console/dist is a gitignored local build that 'turbo run build' does not refresh — ` + - `rebuild it with 'pnpm objectui:build'.`, + `rebuild it with 'pnpm objectui:build'.` + ); +} + +/** + * The refusal block — `os dev` declines to mount a drifted console. Mirrors + * the remediation of `scripts/check-console-sha.mjs` verbatim (rebuild at the + * *pinned* SHA with `pnpm objectui:build`; `objectui:refresh` would re-bump + * the pin to the local ../objectui HEAD, which is the opposite of the fix). + */ +export function formatConsoleShaDriftRefusal(drift: ConsoleShaDrift): string { + return ( + `\n ✗ Console version drift — refusing to serve /_console in dev.\n\n` + + ` pinned (${drift.pinFile}): objectui@${drift.pin.slice(0, 12)}\n` + + ` built (console/dist/.objectui-sha): objectui@${drift.stamp.slice(0, 12)}\n\n` + + ` packages/console/dist is a gitignored local build that 'turbo run build' does NOT refresh,\n` + + ` so this server would serve a Console SPA the repo no longer pins — and anything you\n` + + ` observed in it would describe a different objectui commit.\n\n` + + ` Rebuild the console at the pinned SHA:\n\n` + + ` pnpm objectui:build\n\n` + + ` (Use 'pnpm objectui:refresh' only when you intend to move the pin to your local ../objectui HEAD.)\n` + + ` To boot anyway with the stale bundle: ${DRIFT_OVERRIDE_ENV}=1 — the API still serves either way.\n` ); } +/** Env switch that downgrades the dev refusal back to a warning. */ +export const DRIFT_OVERRIDE_ENV = 'OS_ALLOW_CONSOLE_DRIFT'; + +function driftOverridden(env: NodeJS.ProcessEnv): boolean { + const v = String(env[DRIFT_OVERRIDE_ENV] ?? '').trim().toLowerCase(); + return v === '1' || v === 'true' || v === 'yes' || v === 'on'; +} + +/** + * Decide whether the Console SPA may mount, given what resolution found. + * + * The drift guard has two seats and this is the second one. The first — + * `pnpm check:console-sha` — is wired into the root `pnpm dev` / + * `dev:showcase` / `dev:crm` / `dev:todo` scripts, and every boot that does + * not go through those scripts misses it: `objectstack dev` run directly in + * an example dir, an example's own `dev` script, a `.claude/launch.json` + * config, `pnpm exec objectstack dev`. That is how a QA sweep came to measure + * a console two days behind the pin (#7752) — the pin had moved, the local + * dist had not, and the boot path carried no guard. So the dev server itself + * refuses to mount a console it can prove is not a build of the pin: the + * stale bundle is unreachable rather than silently authoritative, and the API + * keeps serving so api/cli work is unaffected. + * + * Scope is deliberately narrow — `isDev` only, and only on proven drift + * (see `detectConsoleShaDrift`). Published installs carry no pin, so no + * production or cloud deployment can reach the refusal. + */ +export function decideConsoleMount(input: { + /** Whether the resolved package has a built `dist/index.html`. */ + hasDist: boolean; + /** Proven drift, or null. */ + drift: ConsoleShaDrift | null; + /** True under `objectstack dev` (`serve --dev`). */ + isDev: boolean; + /** Defaults to `process.env`. */ + env?: NodeJS.ProcessEnv; +}): { mount: boolean; refusedForDrift: boolean } { + const { hasDist, drift, isDev } = input; + if (!hasDist) return { mount: false, refusedForDrift: false }; + if (drift && isDev && !driftOverridden(input.env ?? process.env)) { + return { mount: false, refusedForDrift: true }; + } + return { mount: true, refusedForDrift: false }; +} + // ─── Plugin Factory ───────────────────────────────────────────────── /** diff --git a/packages/cli/test/console-resolve.test.ts b/packages/cli/test/console-resolve.test.ts index de1eefbe4c..4e5852af5c 100644 --- a/packages/cli/test/console-resolve.test.ts +++ b/packages/cli/test/console-resolve.test.ts @@ -18,7 +18,8 @@ import { createRequire } from 'module'; import { resolveConsolePath, isConsoleVersionCompatible, - warnOnConsoleShaDrift, + detectConsoleShaDrift, + formatConsoleShaDriftWarning, } from '../src/utils/console.js'; // resolveConsolePath() also discovers the real, version-locked workspace @@ -158,10 +159,16 @@ describe('resolveConsolePath version guard', () => { }); }); -describe('warnOnConsoleShaDrift', () => { +describe('detectConsoleShaDrift', () => { const SHA_A = '2b86379384f0f6e99d9a5bb81d73017fd6f99cef'; const SHA_B = '69d6b94419bcaa11223344556677889900aabbcc'; + /** What resolution reports for a drifted candidate, as the message it renders. */ + const warningsFor = (consoleDir: string): string[] => { + const drift = detectConsoleShaDrift(consoleDir); + return drift ? [formatConsoleShaDriftWarning(drift)] : []; + }; + /** * Lay out a monorepo-shaped tree: /.objectui-sha is the pin, and * /packages/console/dist is the vendored, optionally-stamped build. @@ -184,8 +191,7 @@ describe('warnOnConsoleShaDrift', () => { it('warns when the dist stamp differs from the pin (the pull-without-rebuild case)', () => { const { consoleDir } = makePinnedTree(SHA_A, SHA_B); - const warnings: string[] = []; - warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m)); + const warnings = warningsFor(consoleDir); expect(warnings).toHaveLength(1); expect(warnings[0]).toContain(SHA_A.slice(0, 12)); expect(warnings[0]).toContain(SHA_B.slice(0, 12)); @@ -194,15 +200,13 @@ describe('warnOnConsoleShaDrift', () => { it('is silent when the dist stamp matches the pin', () => { const { consoleDir } = makePinnedTree(SHA_A, SHA_A); - const warnings: string[] = []; - warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m)); + const warnings = warningsFor(consoleDir); expect(warnings).toEqual([]); }); it('is silent for an unstamped dist (pre-guard build / sibling-repo fallback)', () => { const { consoleDir } = makePinnedTree(SHA_A, null); - const warnings: string[] = []; - warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m)); + const warnings = warningsFor(consoleDir); expect(warnings).toEqual([]); }); @@ -215,8 +219,7 @@ describe('warnOnConsoleShaDrift', () => { fs.writeFileSync(path.join(consoleDir, 'dist', 'index.html'), ''); fs.writeFileSync(path.join(consoleDir, 'dist', '.objectui-sha'), `${SHA_B}\n`); - const warnings: string[] = []; - warnOnConsoleShaDrift(consoleDir, (m) => warnings.push(m)); + const warnings = warningsFor(consoleDir); expect(warnings).toEqual([]); }); }); From df56303f71011cf4018427af2964bb64971aedb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 19:38:06 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(cli):=20drop=20the=20dogfood-skill=20?= =?UTF-8?q?note=20from=20this=20PR=20=E2=80=94=20`.claude/skills/**`=20shi?= =?UTF-8?q?ps=20separately=20(#7752)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the maintainer's 2026-08-11 ruling, changes under `.claude/skills/**` and `skills/**` are ADR-level and merge through the human channel. Carrying the runner note here would hold the whole guard behind that channel, so it moves to its own PR (branch `claude/issue-7752-skill-note`) and this one keeps only the code, tests, changeset, and the `RUNNER.md` trap row. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LkvcB2Ei8Mpaa87N2US5wY --- .claude/skills/dogfood-verification/SKILL.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.claude/skills/dogfood-verification/SKILL.md b/.claude/skills/dogfood-verification/SKILL.md index 966c99f969..712bb5951f 100644 --- a/.claude/skills/dogfood-verification/SKILL.md +++ b/.claude/skills/dogfood-verification/SKILL.md @@ -74,15 +74,6 @@ unsaved drafts that block navigation, and dirty the working tree. Isolate up fro (separate origin → needs its own `admin@objectos.ai/admin123` login). Skipping this cost a full spawn-a-fix cycle on an action-create dead-end that was already fixed in objectui main. -- [ ] **A drifted build no longer boots silently (#7752).** `build-console.sh` stamps the - SHA it built from into `packages/console/dist/.objectui-sha`; when that stamp differs - from the repo's `.objectui-sha` pin, `os dev` **refuses to mount `/_console`** and - prints the remediation — `/_console/` 404s and the banner advertises no console URL. - That is a precondition to fix, not a product bug: run **`pnpm objectui:build`** - (rebuild at the *pinned* SHA — `objectui:refresh` would re-bump the pin to your local - `../objectui` HEAD) and reboot. `pnpm check:console-sha` reports the same comparison - without booting. `OS_ALLOW_CONSOLE_DRIFT=1` boots the stale bundle deliberately — and - then every console observation you make describes a commit the repo does not pin. ## 3. Verify — visual / API first, DOM last (the anti-false-positive rule)