Skip to content

fix(cli): with no built Console, /_console/ answers 503 naming the remedy instead of a bare 404 - #22419

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22225-console-not-built-loud
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22225-console-not-built-loud

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22225
Clause-②: no

What changes

When the Console package os serve resolves has no built dist/, the server printed the "Console dist not found" boot warning and mounted nothing at the HTTP surface. GET /_console/ then answered the router's ENDPOINT_NOT_FOUND, which looks exactly like a wrong URL (AGENTS.md, Route & surface ownership rule 3, "Absence must be loud").

On that same boot branch, os serve now also mounts createConsoleNotBuiltPlugin(consolePath) (packages/cli/src/utils/console.ts, internal, not on the ./console subpath). It mounts the three routes the Console static plugin would mount:

route built Console (unchanged) no built Console (new)
GET /_console/* the SPA, from dist/ 503, text/plain, cache-control: no-store: the Console is not built, then the remedy
GET /_console 302 to /_console/ 302 to /_console/
GET / 302 to /_console/ 302 to /_console/

One remedy picker. formatConsoleDistMissingWarning now renders a private helper, describeConsoleDistMissing(consolePath, hostPaths). That helper holds the one findConsoleBuildRoot decision and the one copy of the remedy prose. The boot warning calls it with hostPaths: true, and its bytes are unchanged (the existing console-resolve.test.ts pins pass as before). The HTTP answer calls it with hostPaths: false. There is no second picker and no copied sentence.

GET /: redirect. This is the card's design call. / redirects to /_console/ whether or not the Console is built. Three reasons:

  • The route keeps one owner and one answer: the Console's. The not-built plugin claims / on exactly the boots where the static plugin would have claimed it. It registers at the same point in serve.ts, so it shadows nothing the built case did not already shadow.
  • Building the Console and restarting changes only what /_console/* returns.
  • A browser following / lands on the not-built page.

Status 503, not 404. The address is right, and the server is missing a build artifact. The operator fixes that, not the caller. A caller that reads only the status code can still tell this apart from a wrong URL.

Plain text, not a JSON envelope. These routes are the Console's static-asset family in console-route-ledger.ts, not API surface. The 503 body is read by a browser or by curl, and a JSON envelope would put an API-shaped body on a route the ledger keeps static-asset only.

What an anonymous caller is told (the card's H4)

Anyone who can reach the port can read the HTTP answer, with no credential. The terminal warning is read by the operator. So the HTTP form leaves out every absolute path on the host, and keeps the remedy:

  • Framework checkout: Console dist not found at packages/console/dist — ... Build it with pnpm objectui:build in the root of the ObjectStack checkout this server runs from (...). The terminal names the absolute root; the HTTP form names the repository-relative packages/console/dist, which is the repo's public layout.
  • Installed CLI: Console dist not found — the Console ships prebuilt in @objectstack/console, a dependency of @objectstack/cli; reinstall it at the same version as the CLI. The terminal names the absolute .../node_modules/.../dist path, which can reveal the install layout and version. The HTTP form drops it.

Help text

The --ui help text in serve.ts, the packages/cli/README.md row and the mount comment in serve.ts now name @objectstack/console, the package resolveConsolePath reads, instead of @object-ui/console.

Measured on the real composition

Each boot ran os serve from source (bin/run-dev.js through tsx) on a bare export default {}; config, with OS_DATABASE_URL=:memory:. The worktree's packages/console has no dist/, so it is the console package the CLI resolves. Responses were read with curl -i.

boot GET /_console/ GET / GET /_console boot output
before: this head with the two not-built mount lines in serve.ts ablated (BASE's composition; restore proven blob-equal to HEAD) 404 ENDPOINT_NOT_FOUND 404 ENDPOINT_NOT_FOUND 404 ENDPOINT_NOT_FOUND warning printed once
after, no dist 503, the body below 302 to /_console/ 302 to /_console/ warning printed once; ConsoleNotBuilt in the plugin list; no Console: banner row
after, --no-ui 404 ENDPOINT_NOT_FOUND 404 ENDPOINT_NOT_FOUND n/a no warning, nothing mounted
after, control: a fixture @objectstack/console with dist/index.html in the cwd 200 text/html, the shell with the base tag injected 302 to /_console/ 302 to /_console/ ConsoleUI loaded, Console: banner row

GET /api/v1/health answered 200 on the no-dist boot. The not-built body, as served:

The ObjectStack Console is not built, so this server has no Console to serve at /_console/.

Console dist not found at packages/console/dist — `pnpm build` does not produce it. Build it with `pnpm objectui:build` in the root of the ObjectStack checkout this server runs from (uses a ../objectui checkout if present, else clones objectui at the pinned commit — needs network).

Then restart the server. The API is served either way.

The card's measurement had GET / returning an empty 404 body. At BASE through this composition, it answered the same ENDPOINT_NOT_FOUND JSON as /_console/. Either way it was indistinguishable from a wrong URL.

Tests

packages/cli/src/utils/console.not-built.test.ts (unit tier) runs the real plugins on a real HonoHttpServer:

  • Framework tree: GET /_console/, a client-side route and a hashed-asset path each answer 503, text/plain and no-store. Each body's first line is the not-built headline, and each names pnpm objectui:build. The body is the boot warning's sentence with only the absolute build root replaced, and it does not contain that root.
  • Install tree: the body is the warning's sentence with the dist path removed. It names @objectstack/console, not pnpm objectui:build, and contains no path of the project.
  • Redirects: / and /_console redirect to /_console/ in both trees.
  • Controls: a built dist served by createConsoleStaticPlugin answers 200 with the shell and the same redirects. With no Console plugin mounted, / and /_console/ are the bare 404.

test/published-subpath-console.pin.test.ts lists the new export in INTERNAL_SINCE_RETIREMENT, so the partition of utils/console.ts still accounts for every export and the public face stays exactly three names.

Three ablations each made the test fail, as expected. Each was applied with scripts/ablation-replace.mjs: anchor hit 1 to 0, blob changed, and restore proven blob-equal to HEAD with git diff HEAD empty. The test imports ./console.js relatively, so it reads src/ and no dist/ was involved.

ablation result
delete the /_console/* 503 mount 5 failed / 4 passed
render the HTTP answer with hostPaths: true 2 failed / 7 passed (both no-host-path assertions)
drop the / redirect 2 failed / 7 passed (both redirect tests)

Verification (all at 813ff4827, this PR's head)

  • Build. The dependency closure was built with pnpm turbo run build --filter='@objectstack/cli^...' --concurrency=2 (58/58 tasks), then pnpm --filter @objectstack/cli build. Lock verdict for both: VERDICT command-exit 0.
  • Unit tests. pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 272 passed (272), Tests 4003 passed (4003), VERDICT command-exit 0. vitest list --project unit confirms that console.not-built.test.ts, published-subpath-console.pin.test.ts, console-route-ledger.conformance.test.ts and console-resolve.test.ts are in that run.
  • Typecheck. pnpm --filter @objectstack/cli typecheck (tsc --noEmit, then check:test-typecheck over tsconfig.test.json) exited 0. The test-layer debt held, with no new error.
  • Integration tests: left to CI. This diff changes no integration-tier file, no bin/ entry and no test/helpers/serve-process.ts. No queue-tier integration test boots serve with the Console enabled: runServe pins OS_DISABLE_CONSOLE=1, and a grep of the 98 integration files for serve/dev/start spawns that set neither OS_DISABLE_CONSOLE nor --no-ui turned up only flow-node 'start' ids and a dev refusal pin.
  • Dispatch gates. The gate lead's 66 lines, plus pnpm check:cli-test-child-env (added when the list was re-derived with no paths at this head), all exited 0. dispatch-gates --ran says: 67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3).
  • Lint (pnpm lint), narrowed to the changed files. eslint --no-inline-config --format json over the 6 changed files: eslint's own config linted 4 (serve.ts, console.ts and the two tests) with 0 errors and 0 warnings. It ignored the 2 .md files ("File ignored because no matching configuration was supplied"). The narrowing loses nothing: eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules), and the only files it reads at load are baselines this diff does not touch. So no untouched file's result can change. The full pnpm lint run is CI's.
  • Main has moved. origin/main gained 4 commits since b1f7a7a73. None touches a file in this diff, and git merge-tree merges cleanly.

Acceptance notes

  • console-route-ledger.ts notes, not amended here. GET /, GET /_console and GET /_console/* are now mounted by two factories in utils/console.ts, on mutually exclusive boots. The ledger rows' route, mountedIn and static-asset disposition stay true, and the conformance census passes. But each row's family and note describe only createConsoleStaticPlugin. The file is outside this card's claimed file surface, so it is left for a follow-up. Suggested note addition per row: "Also mounted by createConsoleNotBuiltPlugin when the console package resolves without a dist/: the same redirect, or for /_console/* a 503 plain-text answer naming the build or reinstall remedy."
  • Same family, not fixed here: a drift-refused dev boot. os serve --dev refuses to mount a Console whose dist/.objectui-sha differs from the repo pin. Measured on a fixture with pin aaaa… and stamp bbbb…: the terminal prints the refusal block, and GET /_console/ and GET / answer 404 ENDPOINT_NOT_FOUND. That is the same bare 404, with a different remedy (formatConsoleShaDriftRefusal, which names an absolute pin-file path). Reported to the dispatching seat.
  • Observations, not filed: GET /forms/:slug (the public form short link) is mounted only with a built Console, so with no build it falls through to 404. If the console package does not resolve at all (consolePath is null without --no-console), nothing is printed and nothing is mounted. That needs a broken install, and no public reach was measured.

Generated by Claude Code

…ot warning's remedy

os serve mounted nothing when the console package resolved without a dist,
so GET /_console/ answered the router's bare not-found and GET / an empty
404. The boot now mounts createConsoleNotBuiltPlugin on that branch: the
Console's three routes, with /_console/* answering 503 text/plain built
from the same remedy picker the boot warning renders (host paths left
out), and / and /_console redirecting to /_console/ as a built Console
does. The --ui help text, the README row and the mount comment name
@objectstack/console.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/cli/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/README.md) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 31 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json abd254508b861da8ed1e96c2a813541b3274fb40 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 54051f97236572a875ec409c17eeb11063c06b53 — the merge of head 813ff4827c09c61b6dc2051270f60e8fc5732ee3 into base abd254508b861da8ed1e96c2a813541b3274fb40, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 54051f97236572a875ec409c17eeb11063c06b53 && git checkout 54051f97236572a875ec409c17eeb11063c06b53
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abd254508b861da8ed1e96c2a813541b3274fb40 813ff4827c09c61b6dc2051270f60e8fc5732ee3 && git checkout -B drift-repro abd254508b861da8ed1e96c2a813541b3274fb40 && git merge --no-ff 813ff4827c09c61b6dc2051270f60e8fc5732ee3

node scripts/docs-audit/affected-docs.mjs --json abd254508b861da8ed1e96c2a813541b3274fb40

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 05:12
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 05:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 86ae119 Oct 9, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22225-console-not-built-loud branch October 9, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants