You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cli: app branding assets are served only from <cwd>/assets (or OS_RUNTIME_ASSETS_DIR) and silently skipped when that directory is absent — an artifact boot from any other directory draws a broken logo and favicon, with no warning #22071
Filing gate: ① product defect with reach measured. Class (a). reach: every signed-in page of an app whose branding.logo / favicon point at /runtime/assets/*, when the server is started outside the app's project directory. Measured on @objectstack/* 17.7.0 by the repo:hotcrm seat (session_01ER8ntXZhYebyQ66aXWdjfT) during the hotcrm 17.7.0 upgrade (hotcrm PR #2008), on the maintainer's word: 「5. 品牌 logo 容易悄悄丢失 … 以上立卡」.
Who acts on it: objectstack triage routes it, likely domain:cli. ⛔ Not a claim.
Measured
hotcrm's app declares branding: { logo: '/runtime/assets/icon.svg', favicon: '/runtime/assets/icon.svg' } (src/sales/apps/crm.app.ts:61–63). The file ships in the repo at assets/icon.svg.
boot
GET /runtime/assets/icon.svg
console
objectstack start --artifact <dir>/objectstack.json, cwd = <dir> holding only the artifact
404
top-left logo is a broken image; no boot line mentions it
same, after copying assets/ into <dir>
200 image/svg+xml
logo drawn
The maintainer noticed it in the screenshots before anyone else did (「左上角 logo是没显示出来吗」).
Code reading (origin/main8caa131e52)
packages/cli/src/commands/serve.ts:3717–3723: runtimeAssetsDir = process.env.OS_RUNTIME_ASSETS_DIR?.trim() || path.resolve(process.cwd(), 'assets'). The comment beside it: "Silently skips if no assets directory exists."
packages/cli/src/utils/console.ts:614–615: if (!fs.existsSync(assetsDir)) return;, so the route is not mounted at all.
The compiled artifact carries no asset files, so an artifact copied to a host, container or other directory without the app's assets/ loses its branding.
Why it matters
The artifact is the deployable unit, and the docs present --artifact and OS_ARTIFACT_PATH as the deployment shape. An app's own logo then depends on a directory that travels separately and that nothing checks.
The failure is silent on both sides: no boot warning, and only an <img> 404 in the browser console.
A direction, for triage to rule on (⛔ not a ruling)
At boot, when a loaded app's branding.logo / favicon names /runtime/assets/<file> and that file is not servable, print one warning naming the app, the file, the directory searched and OS_RUNTIME_ASSETS_DIR.
Longer term: carry the files an app's branding references in the artifact, or a sidecar the artifact names, so the deployable unit is complete.
Not measured
A cloud or Docker deployment. The reading above is the CLI start / serve path only.
Duplicate check
Semantic issue search on objectstack, branding logo runtime assets 404 artifact deployment assets directory not packaged: 0 hits. Positive control: createRuntimeAssetsPlugin resolves in the code reading above.
Dedupe words: runtime assets 404 · branding logo broken · OS_RUNTIME_ASSETS_DIR · assets not in artifact · silently skips assets
Path: the road — publish/install: an artifact deploys with its app's branding | 缺项 | P2
Triage: first grade, bug · priority:p2 · domain:cli · area:devpath · pm:queue. Direction: a boot warning when a loaded app's branding names a runtime asset the server cannot serve
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T13:02Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/cli/src/commands/serve.ts (the runtime assets dir, :3716–:3720, whose comment says it "Silently skips if no assets directory exists") and packages/cli/src/utils/console.ts:615 (if (!fs.existsSync(assetsDir)) return;) ⇒ domain:cli; rationale: the CLI's start/serve path.
Why p2: a broken logo and favicon on every signed-in page, with nothing said on either side. It was measured on 17.7.0 (hotcrm, an artifact boot outside the project directory).
Direction (ruled here):
At boot, when a loaded app's branding.logo or favicon names /runtime/assets/<file> and that file is not servable, print one warning. It names the app, the file, the directory searched and OS_RUNTIME_ASSETS_DIR.
When the assets directory is absent, say so in that same line, not silently.
⛔ Not taken here: carrying the branding files in the artifact. That changes the deployable unit's shape, and only the CLI path is measured. It becomes a card of its own if a measured deployment (cloud, Docker) shows the loss.
Pins:
an artifact boot from a directory without assets/ prints the warning once, naming the file;
the same boot with OS_RUNTIME_ASSETS_DIR set, or assets/ present, prints nothing and serves the file (control).
Clause-②: no: an operator log line only. Patch changeset.
Claim: PM loop round 4
Session: session_01RWZbGvPFcRKvUqASZtunCU
Account: os-warren (the seat's linked user as get_me answers it; the card's assignee)
Branch: claude/issue-22071-runtime-assets-boot-warning
Worktree: objectstack-issue-22071
Domain: domain:cli
Seat: domain:cli#1
File surface, per triage 6038494234, read on origin/main3d918850:
packages/cli/src/commands/serve.ts: the runtime assets block (:3714–:3723, whose comment says "Silently skips if no assets directory exists"), and the one point after boot where the loaded apps' branding.logo / favicon are read for the warning.
packages/cli/src/utils/console.ts: createRuntimeAssetsPlugin (from :603; the if (!fs.existsSync(assetsDir)) return; at :615).
Pins beside them in packages/cli: an artifact boot from a directory without assets/ prints the warning once, naming the app, the file, the directory searched and OS_RUNTIME_ASSETS_DIR. The control: the same boot with OS_RUNTIME_ASSETS_DIR set, or assets/ present, prints nothing and serves the file.
.changeset/*.md: @objectstack/clipatch.
⛔ Not taken: carrying branding files in the artifact (triage). ⛔ No packages/spec path. ⛔ No route added or removed, and no change to what /runtime/assets/* serves. ⛔ Not packages/runtime/src/app-plugin.ts:1104–:1140 (declared by domain:spec seat 2 for Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458, 6038311314). (Stop on breach and explain in the report.)
Container & model: S, mode:subagent, model: default (opus). dispatch-gates --tier over the path gives no path-derived mandate.
Clause-②: no
An operator log line only (triage 6038494234): no accept set, answer or public signature moves.
Responsibility: platform code: @objectstack/cli resolves the runtime assets directory from the cwd and skips the mount without a word (serve.ts:3714–3723, utils/console.ts:615) | none: no boot line or check names a branding asset the server cannot serve | an operator who boots an artifact outside the app project directory; hotcrm reached it on 17.7.0 (the card body measures it), and every app whose branding names /runtime/assets/* can
Thread-read: 6038494234
Serial constraints cleared: read 2026-10-07T13:33Z:
os-dev-report
{
"issue": 22071,
"status": "done",
"branch": "claude/issue-22071-runtime-assets-boot-warning",
"pr": "#22089",
"session": "session_01RWZbGvPFcRKvUqASZtunCU",
"premise_still_valid": true,
"summary": "H1 held on origin/main 3d91885: an artifact booted from a directory without assets/ answered 404 for /runtime/assets/icon.svg, and zero boot lines mentioned it; both controls (assets/ beside the artifact, OS_RUNTIME_ASSETS_DIR) answered 200 image/svg+xml. createRuntimeAssetsPlugin now registers a kernel:bootstrapped hook. The hook reads the served app list, kernel.getService('protocol').getMetaItems({ type: 'app' }), the same call GET /api/v1/meta/app answers from. It logs ONE ctx.logger.warn per branding.logo / branding.favicon URL under /runtime/assets/ that the route will not serve. The line names the apps and keys, the file, the directory searched, OS_RUNTIME_ASSETS_DIR versus the cwd/assets default, and whether the directory exists. The route and the check share one private resolution helper (resolveRuntimeAssetPath), and what /runtime/assets/* serves is unchanged. The channel is the kernel logger only: under the boot-quiet window it is replayed once in Boot diagnostics, never in the banner list. After the change the no-assets boot prints exactly 1 line and the controls print 0.",
"tests": "All at head c397a0f. (1) runtime-assets.test.ts, unit tier, run as pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/runtime-assets.test.ts: 'Tests 12 passed (12)'. (2) serve-runtime-assets-branding-warning.e2e.test.ts, integration project, e2e tier, three real artifact boots, run with OS_TEST_TIERS=nightly: 'Tests 3 passed (3)'. (3) pnpm --filter @objectstack/cli test: 'Test Files 352 passed (352)', 'Tests 4692 passed | 2 skipped (4694)', CLI_TEST_EXIT=0. (4) pnpm --filter @objectstack/cli typecheck: CLI_TYPECHECK_EXIT=0, with 'check:test-typecheck: OK ... 3 file(s) / 28 error(s) / 6 pinned signature(s)' unchanged. Ablations were committed first and run through scripts/ablation-replace.mjs; each leg proved the mutation landed (anchor 1 to 0, blob d34b2164 to a new blob) and the restore (blob == HEAD d34b2164, git diff HEAD empty). Both subjects resolve from src (the unit test imports ../src/utils/console.js; the e2e child runs bin/run-dev.js from source through tsx), so there is no dist/ leg. Ablation A, kernel:bootstrapped renamed to kernel:ablated-22071: unit '9 failed | 3 passed'; e2e no-assets boot red, both controls green. Ablation B, the param decodeURIComponent dropped: unit '1 failed | 11 passed', the route-agreement case. The first unit leg of A was a no-op: under OS_TEST_TIERS=nightly the unit project selected nothing ('FILTER SELECTED NOTHING'). It is not counted, and it was re-run without the switch. H1 before/after repro: three boots each on 3d91885 and on the change; the table is in the PR body.",
"gates": "dispatch-gates --commands over the actual diff derived 67 families, identical to the order's list. All 67 exit 0 at c397a0f. check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: packages without dist/). Both were re-run after turbo run build --filter='!@objectstack/docs' (72 tasks, 71 cached) and both exited 0. --ran reconcile: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN'. pnpm lint (full): exit 0, no findings. Extra, not derived: pnpm check:startup-registry-verdict exit 0 ('43 startup/open-registry seam(s) ... none recording a verdict the boot can contradict'). PR CI: in_progress at report time; not awaited, per os-dev.",
"files_changed": [
".changeset/22071-runtime-assets-boot-warning.md",
"packages/cli/src/commands/serve.ts",
"packages/cli/src/utils/console.ts",
"packages/cli/test/runtime-assets.test.ts",
"packages/cli/test/serve-runtime-assets-branding-warning.e2e.test.ts"
],
"line_budget": "Not applicable: no skills/** file and no governed ledger was touched. Diff vs merge base 3d91885: 5 files, +717 / -26, under the 5000-line human-merge threshold.",
"deviations": [
"Lock time: the order asked for pnpm --filter @objectstack/cli test, which runs the unit AND integration projects. os-dev says a cli card owes only the unit tier locally and declares integration to CI. I ran the full command as the order asked; it held the shared lock about 33 minutes. Noting the conflict, not choosing a side silently.",
"Lint: os-dev calls the full pnpm lint CI-owned and never owed. The order adds it, so it was run (in the background, no foreground cap) and exited 0.",
"Pin tier: the boot-level pin is named *.e2e.test.ts, so it runs on the nightly tier (OS_TEST_TIERS=nightly), not per PR. The per-PR half is the unit file. See open_questions.",
"PR footer: the PR body ends with the AGENTS.md session-URL footer form, not the harness reminder's two-line form, because the repo's own instruction takes precedence.",
"Workspace: the worktree was removed after the PR opened. No labels were written; the order names none, and skip-changeset is forbidden."
],
"mcp_calls": "0 — no MCP GitHub tool was called.",
"api_writes": "3 REST writes, each through the fleet relay. (1) POST /repos/objectstack-ai/objectstack/dispatches carried pr_create, which ran as POST /repos/objectstack-ai/objectstack/pulls and created #22089 (draft; body read back byte-identical, 11375 bytes). (2) POST /repos/objectstack-ai/objectstack/dispatches carried the assignee write, which ran as POST /repos//issues/22089/assignees and set os-warren (read back). (3) This os-dev-report comment, posted with post-stamped as POST /repos//issues/22071/comments. Not REST writes: git push x4 to claude/issue-22071-runtime-assets-boot-warning. Reads: REST GET issues/22071, its comments, issues/22073 and pulls/22089.",
"open_questions": [
{
"question": "Should the end-to-end boot pin (an artifact boot without assets/ warns once; the OS_RUNTIME_ASSETS_DIR and assets/ controls are silent and serve the file) run on every PR, or on the nightly e2e tier where it sits now?",
"options": [
"A: keep serve-runtime-assets-branding-warning.e2e.test.ts on the nightly tier. Per PR, runtime-assets.test.ts pins the plugin logic, including the absent-directory warning, the controls, the matcher and the route agreement. The serve.ts wiring (env vs cwd source) is pinned only nightly.",
"B: rename it to a non-tier name (for example .integration.test.ts) so it runs in the per-PR integration project. That costs about 30 s of three child boots on every cli test run."
],
"recommendation": "A. The maintainer's tiering direction moved spawn-boot tests off the per-PR run. The per-PR unit file goes red under both ablations, and the serve.ts part the nightly alone covers is a 5-line source selection."
}
],
"out_of_scope_findings": [
"class: a · reach: public door, the boot output of os serve. Every artifact boot on a fresh :memory: database printed a Boot diagnostics line, 'WARN Insert operation failed {"object":"sys_migration","error":{"message":"UNIQUE constraint failed: sys_migration.id ..."}}', followed by a full knex stack (createWithAutonumberResync, packages/objectql/src/engine.ts:13822 at 3d91885). · evidence: 6 of 6 boots in this run, 3 of them on origin/main 3d91885 before any change. Repro: the minimal artifact in the PR body booted with OS_ARTIFACT_PATH from an empty dir under bin/run-dev.js serve, OS_DATABASE_URL=:memory:. Possibly the same family as #22073 (boot-output noise), but this is a failed insert, not a repeated sentence. · dedupe words: sys_migration UNIQUE constraint boot · Insert operation failed sys_migration · migration journal duplicate id · boot diagnostics knex stack",
"carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed: examples/app-todo/src/apps/todo.app.ts sets branding.logo '/assets/todo-logo.png' and favicon '/assets/todo-favicon.ico'. Neither file exists in the example, and nothing serves /assets/ at the root. This is a read-only inference (not booted) and is outside the H4 matcher by design."
]
}
domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · read on GitHub 2026-10-07T15:20Z
Checked on GitHub and in the diff, not from the report:
Shape: draft, base main. Line 1 is Fixes #22071, line 2 is Clause-②: no. Assignee os-warren. Labels are the bot's documentation, size/l, tests and tooling. There is no skip-changeset, which is right: one patch changeset ships.
Scope: 5 files, +717 / −26, three commits (fix a992930d, pins 0cbf1220, changeset c397a0f3). The files are serve.ts, utils/console.ts, two test files in packages/cli/test, and the changeset. That is inside claim 6039071657. No packages/spec, and nothing in app-plugin.ts.
serve.ts: the same directory as before (OS_RUNTIME_ASSETS_DIR, else <cwd>/assets). It now also hands createRuntimeAssetsPlugin which of the two it came from. The "Silently skips" comment is replaced by what happens now.
utils/console.ts:
One resolution.resolveRuntimeAssetPath is the route's old inline resolution, moved verbatim: strip slashes, path.join, the traversal guard. The route now serves through it and runtimeAssetIsServable judges through it, so the warning and the route cannot disagree (H3).
Matcher.runtimeAssetRequest takes only a root path that resolves on this origin under /runtime/assets/. Absolute and protocol-relative URLs, /\host, data URIs, relative paths and other root paths are not checked (H4). A subdirectory URL is reported as one.
The check. It hangs on kernel:bootstrapped and reads protocol.getMetaItems({ type: 'app' }), the call the REST meta list answers from (H2). It writes one ctx.logger.warn per unserved URL, naming the apps and keys, the directory searched, its source, and whether the directory exists. A read failure logs at debug and never fails the boot.
The route. It is still not mounted when the directory is absent, and what it serves is unchanged.
Signature:createRuntimeAssetsPlugin gains a required dirSource. It is not a published export: test/published-subpath-console.pin.test.ts:355 pins it as retired from @objectstack/cli/console, and its one caller is serve.ts. patch holds.
The seat checked every factual sentence the changeset adds against the branch:
"objectstack start, which runs it": start.ts spawns the serve child. True.
"streams live at --log-level debug or info": isVerboseBootLevel (boot-log-capture.ts:126) is exactly debug / info. True.
"the same metadata protocol read that GET /api/v1/meta/app answers from": the REST meta list calls getMetaItems (rest-server.ts:6078). True.
The matcher and the "nothing under /runtime/assets/ is mounted" sentences match the code above. True.
Pins, read in the diff:
runtime-assets.test.ts (unit tier, per PR):
the absent directory gives one warning for a file both keys name;
the file-present control is silent;
a present directory with a missing file warns without claiming the directory is absent;
one line per file;
the matcher's negative spellings, with a positive control in the same boot;
route agreement across query, fragment, dot segments and encoding;
the subdirectory case;
the never-fails-the-boot case.
serve-runtime-assets-branding-warning.e2e.test.ts: three real artifact boots, each in its own mkdtemp directory: no assets/ (warns once, 404), assets/ beside the artifact (silent, 200) and OS_RUNTIME_ASSETS_DIR (silent, 200).
The dev's ablations through scripts/ablation-replace.mjs:
the hook renamed: unit 9 red, and the e2e no-assets boot red;
the decode dropped: the route-agreement case red.
Each leg was restored to a blob equal to HEAD.
The dev's open question (where the boot pin runs): the seat answers A, keep it on the nightly e2e tier.
The maintainer's 2026-09-07 direction (packages/cli/vitest.config.ts:596) moved the *.e2e.test.* tier off the per-PR and merge-queue runs.
The per-PR unit file goes red under both ablations. What only the nightly pin covers is serve.ts's 5-line choice of directory source.
dispatch-gates --ran accounts for 67 of 67 derived families. pnpm lint exits 0.
The seat's own reading (standing lesson 1, a new mkdtemp site):node scripts/pm/dispatch-gates.mjs --self-test, run by the seat in a worktree at c397a0f3, printed ✓ dispatch-gates self-test: 1976 cases pass. and exited 0 (read 2026-10-07T15:07Z).
examples/app-todo's /assets/… branding stays an Acceptance note: it is not measured, and it is outside this check by design.
CI on c397a0f3: read 2026-10-07T15:19Z: 29 success · 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke) · 1 in progress (Test Core 1/6) · 0 red. That is an honest reading, ⛔ not green. GitHub answered HTTP 500 on this commit's check-runs from about 15:08Z to 15:18Z, so the earlier reads could not be taken.
After green: the landing pre-checks, then the relay landing.
git rev-list --parents -n 1 bafb58bb08 names one parent, b04a5295f7.
bafb58bb08 is an ancestor of origin/main; the pre-merge head c397a0f3 is not.
It entered the merge queue 2026-10-07T15:29:59Z and merged 2026-10-07T15:52:23Z. Fixes #22071 closed this card as completed.
Content on origin/main:
packages/cli/src/utils/console.ts carries resolveRuntimeAssetPath (the one resolution the route serves through and the boot check judges through) and the kernel:bootstrapped check;
serve.ts hands createRuntimeAssetsPlugin the directory's source;
.changeset/22071-runtime-assets-boot-warning.md (patch) is present.
Review of record: ACCEPT 6040954239. Every new factual sentence in the changeset was checked against the code. Clause-②: no; no contract review was owed. The seat's own dispatch-gates --self-test at c397a0f3: 1976 cases pass.
Delivered: a boot warning when a loaded app's branding.logo / favicon names a /runtime/assets/<file> this boot will not serve. It names the apps and keys, the directory searched, its source (OS_RUNTIME_ASSETS_DIR or the <cwd>/assets default), and whether the directory exists. What /runtime/assets/* serves is unchanged.
Not taken, by triage: carrying the branding files in the artifact. It becomes its own card if a measured cloud or Docker deployment shows the loss.
Carried as Acceptance notes:examples/app-todo's /assets/… branding is not checked by design (not /runtime/assets/), and the boot check reads the environment-wide app list.
Activity
objectstack-fleet commentedon Oct 7, 2026
Path: the road — publish/install: an artifact deploys with its app's branding | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:cli·area:devpath·pm:queue. Direction: a boot warning when a loaded app's branding names a runtime asset the server cannot serveTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T13:02Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/cli/src/commands/serve.ts(the runtime assets dir,:3716–:3720, whose comment says it "Silently skips if no assets directory exists") andpackages/cli/src/utils/console.ts:615(if (!fs.existsSync(assetsDir)) return;) ⇒domain:cli; rationale: the CLI'sstart/servepath.branding.logoorfaviconnames/runtime/assets/<file>and that file is not servable, print one warning. It names the app, the file, the directory searched andOS_RUNTIME_ASSETS_DIR.assets/prints the warning once, naming the file;OS_RUNTIME_ASSETS_DIRset, orassets/present, prints nothing and serves the file (control).Clause-②: no: an operator log line only. Patch changeset.objectstack-fleet commentedon Oct 7, 2026
Claim: PM loop round 4
Session:
session_01RWZbGvPFcRKvUqASZtunCUAccount:
os-warren(the seat's linked user asget_meanswers it; the card's assignee)Branch:
claude/issue-22071-runtime-assets-boot-warningWorktree:
objectstack-issue-22071Domain:
domain:cliSeat:
domain:cli#1File surface, per triage
6038494234, read onorigin/main3d918850:packages/cli/src/commands/serve.ts: the runtime assets block (:3714–:3723, whose comment says "Silently skips if no assets directory exists"), and the one point after boot where the loaded apps'branding.logo/faviconare read for the warning.packages/cli/src/utils/console.ts:createRuntimeAssetsPlugin(from:603; theif (!fs.existsSync(assetsDir)) return;at:615).packages/cli: an artifact boot from a directory withoutassets/prints the warning once, naming the app, the file, the directory searched andOS_RUNTIME_ASSETS_DIR. The control: the same boot withOS_RUNTIME_ASSETS_DIRset, orassets/present, prints nothing and serves the file..changeset/*.md:@objectstack/clipatch.packages/specpath. ⛔ No route added or removed, and no change to what/runtime/assets/*serves. ⛔ Notpackages/runtime/src/app-plugin.ts:1104–:1140(declared bydomain:specseat 2 for Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458,6038311314). (Stop on breach and explain in the report.)Container & model:
S,mode:subagent,model: default (opus).dispatch-gates --tierover the path gives no path-derived mandate.Clause-②: no
6038494234): no accept set, answer or public signature moves.Responsibility:
platform code: @objectstack/cli resolves the runtime assets directory from the cwd and skips the mount without a word (serve.ts:3714–3723, utils/console.ts:615)|none: no boot line or check names a branding asset the server cannot serve|an operator who boots an artifact outside the app project directory; hotcrm reached it on 17.7.0 (the card body measures it), and every app whose branding names /runtime/assets/* canThread-read: 6038494234
Serial constraints cleared: read 2026-10-07T13:33Z:
filename): neither touchesserve.tsorutils/console.ts. chore: version packages (next) #21988 is the release PR; feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 holdsdomain:specandservice-automationfiles.sharing.publicLinkreads as the "Generated public share URL" but is only a slug — the authored path (/forms/contact-us) answers 404, and the real anonymous URL (/_console/f/<slug>) is shown to the author nowhere #22079 may add a route inutils/console.ts's console plugin andserve.ts's console mount, and boot output: the "schedule trigger is NOT bound" sentence (~600 chars) prints twice for every scheduled flow — 16 of an 8-flow app's boot lines — and the loopback OAuth-over-HTTP warning fires on every localhost boot; one summary line per warning class #22073 may touchserve.tswhere boot diagnostics reach the banner. Those are different regions of paths outsideSINGLE_CLAIM_PATHS(only.objectui-sha), so this is ordinary concurrency and the later lander resolves.domain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-07T13:33Zobjectstack-fleet commentedon Oct 7, 2026
os-dev-report
{
"issue": 22071,
"status": "done",
"branch": "claude/issue-22071-runtime-assets-boot-warning",
"pr": "#22089",
"session": "session_01RWZbGvPFcRKvUqASZtunCU",
"premise_still_valid": true,
"summary": "H1 held on origin/main 3d91885: an artifact booted from a directory without assets/ answered 404 for /runtime/assets/icon.svg, and zero boot lines mentioned it; both controls (assets/ beside the artifact, OS_RUNTIME_ASSETS_DIR) answered 200 image/svg+xml. createRuntimeAssetsPlugin now registers a kernel:bootstrapped hook. The hook reads the served app list, kernel.getService('protocol').getMetaItems({ type: 'app' }), the same call GET /api/v1/meta/app answers from. It logs ONE ctx.logger.warn per branding.logo / branding.favicon URL under /runtime/assets/ that the route will not serve. The line names the apps and keys, the file, the directory searched, OS_RUNTIME_ASSETS_DIR versus the cwd/assets default, and whether the directory exists. The route and the check share one private resolution helper (resolveRuntimeAssetPath), and what /runtime/assets/* serves is unchanged. The channel is the kernel logger only: under the boot-quiet window it is replayed once in Boot diagnostics, never in the banner list. After the change the no-assets boot prints exactly 1 line and the controls print 0.",
"tests": "All at head c397a0f. (1) runtime-assets.test.ts, unit tier, run as pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/runtime-assets.test.ts: 'Tests 12 passed (12)'. (2) serve-runtime-assets-branding-warning.e2e.test.ts, integration project, e2e tier, three real artifact boots, run with OS_TEST_TIERS=nightly: 'Tests 3 passed (3)'. (3) pnpm --filter @objectstack/cli test: 'Test Files 352 passed (352)', 'Tests 4692 passed | 2 skipped (4694)', CLI_TEST_EXIT=0. (4) pnpm --filter @objectstack/cli typecheck: CLI_TYPECHECK_EXIT=0, with 'check:test-typecheck: OK ... 3 file(s) / 28 error(s) / 6 pinned signature(s)' unchanged. Ablations were committed first and run through scripts/ablation-replace.mjs; each leg proved the mutation landed (anchor 1 to 0, blob d34b2164 to a new blob) and the restore (blob == HEAD d34b2164, git diff HEAD empty). Both subjects resolve from src (the unit test imports ../src/utils/console.js; the e2e child runs bin/run-dev.js from source through tsx), so there is no dist/ leg. Ablation A, kernel:bootstrapped renamed to kernel:ablated-22071: unit '9 failed | 3 passed'; e2e no-assets boot red, both controls green. Ablation B, the param decodeURIComponent dropped: unit '1 failed | 11 passed', the route-agreement case. The first unit leg of A was a no-op: under OS_TEST_TIERS=nightly the unit project selected nothing ('FILTER SELECTED NOTHING'). It is not counted, and it was re-run without the switch. H1 before/after repro: three boots each on 3d91885 and on the change; the table is in the PR body.",
"gates": "dispatch-gates --commands over the actual diff derived 67 families, identical to the order's list. All 67 exit 0 at c397a0f. check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: packages without dist/). Both were re-run after turbo run build --filter='!@objectstack/docs' (72 tasks, 71 cached) and both exited 0. --ran reconcile: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN'. pnpm lint (full): exit 0, no findings. Extra, not derived: pnpm check:startup-registry-verdict exit 0 ('43 startup/open-registry seam(s) ... none recording a verdict the boot can contradict'). PR CI: in_progress at report time; not awaited, per os-dev.",
"files_changed": [
".changeset/22071-runtime-assets-boot-warning.md",
"packages/cli/src/commands/serve.ts",
"packages/cli/src/utils/console.ts",
"packages/cli/test/runtime-assets.test.ts",
"packages/cli/test/serve-runtime-assets-branding-warning.e2e.test.ts"
],
"line_budget": "Not applicable: no skills/** file and no governed ledger was touched. Diff vs merge base 3d91885: 5 files, +717 / -26, under the 5000-line human-merge threshold.",
"deviations": [
"Lock time: the order asked for pnpm --filter @objectstack/cli test, which runs the unit AND integration projects. os-dev says a cli card owes only the unit tier locally and declares integration to CI. I ran the full command as the order asked; it held the shared lock about 33 minutes. Noting the conflict, not choosing a side silently.",
"Lint: os-dev calls the full pnpm lint CI-owned and never owed. The order adds it, so it was run (in the background, no foreground cap) and exited 0.",
"Pin tier: the boot-level pin is named *.e2e.test.ts, so it runs on the nightly tier (OS_TEST_TIERS=nightly), not per PR. The per-PR half is the unit file. See open_questions.",
"PR footer: the PR body ends with the AGENTS.md session-URL footer form, not the harness reminder's two-line form, because the repo's own instruction takes precedence.",
"Workspace: the worktree was removed after the PR opened. No labels were written; the order names none, and skip-changeset is forbidden."
],
"mcp_calls": "0 — no MCP GitHub tool was called.",
"api_writes": "3 REST writes, each through the fleet relay. (1) POST /repos/objectstack-ai/objectstack/dispatches carried pr_create, which ran as POST /repos/objectstack-ai/objectstack/pulls and created #22089 (draft; body read back byte-identical, 11375 bytes). (2) POST /repos/objectstack-ai/objectstack/dispatches carried the assignee write, which ran as POST /repos//issues/22089/assignees and set os-warren (read back). (3) This os-dev-report comment, posted with post-stamped as POST /repos//issues/22071/comments. Not REST writes: git push x4 to claude/issue-22071-runtime-assets-boot-warning. Reads: REST GET issues/22071, its comments, issues/22073 and pulls/22089.",
"open_questions": [
{
"question": "Should the end-to-end boot pin (an artifact boot without assets/ warns once; the OS_RUNTIME_ASSETS_DIR and assets/ controls are silent and serve the file) run on every PR, or on the nightly e2e tier where it sits now?",
"options": [
"A: keep serve-runtime-assets-branding-warning.e2e.test.ts on the nightly tier. Per PR, runtime-assets.test.ts pins the plugin logic, including the absent-directory warning, the controls, the matcher and the route agreement. The serve.ts wiring (env vs cwd source) is pinned only nightly.",
"B: rename it to a non-tier name (for example .integration.test.ts) so it runs in the per-PR integration project. That costs about 30 s of three child boots on every cli test run."
],
"recommendation": "A. The maintainer's tiering direction moved spawn-boot tests off the per-PR run. The per-PR unit file goes red under both ablations, and the serve.ts part the nightly alone covers is a 5-line source selection."
}
],
"out_of_scope_findings": [
"class: a · reach: public door, the boot output of os serve. Every artifact boot on a fresh :memory: database printed a Boot diagnostics line, 'WARN Insert operation failed {"object":"sys_migration","error":{"message":"UNIQUE constraint failed: sys_migration.id ..."}}', followed by a full knex stack (createWithAutonumberResync, packages/objectql/src/engine.ts:13822 at 3d91885). · evidence: 6 of 6 boots in this run, 3 of them on origin/main 3d91885 before any change. Repro: the minimal artifact in the PR body booted with OS_ARTIFACT_PATH from an empty dir under bin/run-dev.js serve, OS_DATABASE_URL=:memory:. Possibly the same family as #22073 (boot-output noise), but this is a failed insert, not a repeated sentence. · dedupe words: sys_migration UNIQUE constraint boot · Insert operation failed sys_migration · migration journal duplicate id · boot diagnostics knex stack",
"carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed: examples/app-todo/src/apps/todo.app.ts sets branding.logo '/assets/todo-logo.png' and favicon '/assets/todo-favicon.ico'. Neither file exists in the example, and nothing serves /assets/ at the root. This is a read-only inference (not booted) and is outside the H4 matcher by design."
]
}
objectstack-fleet commentedon Oct 7, 2026
ACCEPT — PR #22089 at
c397a0f3, pending CIdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· read on GitHub 2026-10-07T15:20ZChecked on GitHub and in the diff, not from the report:
main. Line 1 isFixes #22071, line 2 isClause-②: no. Assigneeos-warren. Labels are the bot'sdocumentation,size/l,testsandtooling. There is noskip-changeset, which is right: onepatchchangeset ships.a992930d, pins0cbf1220, changesetc397a0f3). The files areserve.ts,utils/console.ts, two test files inpackages/cli/test, and the changeset. That is inside claim6039071657. Nopackages/spec, and nothing inapp-plugin.ts.serve.ts: the same directory as before (OS_RUNTIME_ASSETS_DIR, else<cwd>/assets). It now also handscreateRuntimeAssetsPluginwhich of the two it came from. The "Silently skips" comment is replaced by what happens now.utils/console.ts:resolveRuntimeAssetPathis the route's old inline resolution, moved verbatim: strip slashes,path.join, the traversal guard. The route now serves through it andruntimeAssetIsServablejudges through it, so the warning and the route cannot disagree (H3).runtimeAssetRequesttakes only a root path that resolves on this origin under/runtime/assets/. Absolute and protocol-relative URLs,/\host, data URIs, relative paths and other root paths are not checked (H4). A subdirectory URL is reported as one.kernel:bootstrappedand readsprotocol.getMetaItems({ type: 'app' }), the call the REST meta list answers from (H2). It writes onectx.logger.warnper unserved URL, naming the apps and keys, the directory searched, its source, and whether the directory exists. A read failure logs atdebugand never fails the boot.createRuntimeAssetsPlugingains a requireddirSource. It is not a published export:test/published-subpath-console.pin.test.ts:355pins it as retired from@objectstack/cli/console, and its one caller isserve.ts.patchholds.objectstack start, which runs it":start.tsspawns theservechild. True.--log-level debugorinfo":isVerboseBootLevel(boot-log-capture.ts:126) is exactlydebug/info. True.GET /api/v1/meta/appanswers from": the REST meta list callsgetMetaItems(rest-server.ts:6078). True./runtime/assets/is mounted" sentences match the code above. True.Pins, read in the diff:
runtime-assets.test.ts(unit tier, per PR):serve-runtime-assets-branding-warning.e2e.test.ts: three real artifact boots, each in its ownmkdtempdirectory: noassets/(warns once, 404),assets/beside the artifact (silent, 200) andOS_RUNTIME_ASSETS_DIR(silent, 200).scripts/ablation-replace.mjs:The dev's open question (where the boot pin runs): the seat answers A, keep it on the nightly
e2etier.packages/cli/vitest.config.ts:596) moved the*.e2e.test.*tier off the per-PR and merge-queue runs.serve.ts's 5-line choice of directory source.Evidence (the dev's, at
c397a0f3):@objectstack/clitests: 352 files, 4692 passed and 2 skipped. Typecheck exits 0.dispatch-gates --ranaccounts for 67 of 67 derived families.pnpm lintexits 0.The seat's own reading (standing lesson 1, a new
mkdtempsite):node scripts/pm/dispatch-gates.mjs --self-test, run by the seat in a worktree atc397a0f3, printed✓ dispatch-gates self-test: 1976 cases pass.and exited 0 (read 2026-10-07T15:07Z).Out-of-scope findings:
sys_migrationUNIQUE-constraint warning on every fresh:memory:boot is filed as [finding] every artifact boot on a fresh:memory:database logsInsert operation failed … UNIQUE constraint failed: sys_migration.idwith a full knex stack in Boot diagnostics #22099 (finding, for triage).examples/app-todo's/assets/…branding stays an Acceptance note: it is not measured, and it is outside this check by design.CI on
c397a0f3: read 2026-10-07T15:19Z: 29 success · 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke) · 1 in progress (Test Core 1/6) · 0 red. That is an honest reading, ⛔ not green. GitHub answered HTTP 500 on this commit's check-runs from about 15:08Z to 15:18Z, so the earlier reads could not be taken.After green: the landing pre-checks, then the relay landing.
objectstack-fleet commentedon Oct 7, 2026
Landed: PR #22089 →
bafb58bb08, a single-parent queue squashdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-07T15:52Zgit rev-list --parents -n 1 bafb58bb08names one parent,b04a5295f7.bafb58bb08is an ancestor oforigin/main; the pre-merge headc397a0f3is not.Fixes #22071closed this card as completed.origin/main:packages/cli/src/utils/console.tscarriesresolveRuntimeAssetPath(the one resolution the route serves through and the boot check judges through) and thekernel:bootstrappedcheck;serve.tshandscreateRuntimeAssetsPluginthe directory's source;.changeset/22071-runtime-assets-boot-warning.md(patch) is present.6040954239. Every new factual sentence in the changeset was checked against the code.Clause-②: no; no contract review was owed. The seat's owndispatch-gates --self-testatc397a0f3: 1976 cases pass.branding.logo/faviconnames a/runtime/assets/<file>this boot will not serve. It names the apps and keys, the directory searched, its source (OS_RUNTIME_ASSETS_DIRor the<cwd>/assetsdefault), and whether the directory exists. What/runtime/assets/*serves is unchanged.:memory:database logsInsert operation failed … UNIQUE constraint failed: sys_migration.idwith a full knex stack in Boot diagnostics #22099 (finding). Every fresh:memory:artifact boot logsInsert operation failed … UNIQUE constraint failed: sys_migration.idwith a knex stack. It reproduces onorigin/mainbefore this change. Triage grades it.examples/app-todo's/assets/…branding is not checked by design (not/runtime/assets/), and the boot check reads the environment-wide app list.fix(cli): warn at boot when an app's branding names a runtime asset t…
fix(cli,plugin-auth): one boot line per warning class, each printed o…