Repository navigation
Description
Observation-class finding, no behaviour change requested. Found while adopting the artifact-pinned boot (#8368) on the ObjectOS EE image (cloud#1292). Filed unassigned.
What happens
With OS_ARTIFACT_URL set, serve resolves the reference, sets useArtifactFallback = true, and loads { mod: { default: {} } } — the objectstack.config.ts in the cwd is deliberately never executed. The boot diagnostics say so plainly:
Artifact (OS_ARTIFACT_URL): http://127.0.0.1:41541/hotcrm-2.2.2.json [remote, sha256 verified]
Booting from the artifact named by OS_ARTIFACT_URL (default host)...
But the ready banner a few lines later still reports:
Config: objectstack.config.ts
Mode: production
Driver: SqlDriver(better-sqlite3) → :memory:
printServerReady({ configFile: relativeConfig, … }) is passed relativeConfig unconditionally, and relativeConfig is derived from args.config at the top of run() — before the artifact branch is decided. So the row names a file that exists, was found, and was not read.
Why it is worth a line of code
The banner is the surface an operator uses to answer "what is this container actually running", and on this path that is the whole question the feature turns on — the two-axis model means the config in the image and the artifact in the env are different apps. A row that names the file that did not boot points the reader at the wrong one, and it does it on the same screen that just said the opposite. It cost a reading during the cloud-side adoption: the row is the first thing that looks authoritative, and the plugin table is what actually settles it.
The neighbouring precedent is #4801 (cloud#1020), where the banner's tenancy row disagreed with the runtime wiring and every later investigation paid for it.
Suggested shape
Report the artifact on this path instead of the config — the resolved display is already in hand (pinnedArtifact.display), already redacted for a pre-signed URL, and already printed by the diagnostic line above. Something like Artifact: <display> (OS_ARTIFACT_URL) in the Config: slot, or an added row with the Config: row omitted when useArtifactFallback is set.
Same for the plain artifact-fallback path (no config authored, dist/objectstack.json), where the row currently names a config file that does not exist.
Reproduce
objectstack build
OS_ARTIFACT_URL=file://$PWD/dist/objectstack.json objectstack serve objectstack.config.ts
# banner: Config: objectstack.config.ts ← never loadedVerified at 8798cd2a60649f9a54d794a97da314d41be4ab7b.
Related: #8368, cloud#1292.
Activity
os-project-manager commentedon Aug 16, 2026
Triage (first-touch grading): promoted
finding→pm:queue,domain:cli(lands on thepackages/cliserve banner path), type Bug. Rationale: named landing site (printServerReadyis fedrelativeConfigunconditionally, decided before the artifact branch), repro included, and the misreport already cost a reading during the cloud-side adoption — precedent #4801 shows this banner-disagrees-with-wiring class is expensive downstream. The suggested shape in the card (reportpinnedArtifact.displayon the artifact paths, cover the plain artifact-fallback path too) is a suggestion, not a ruling.Size/model suggestion: S, sonnet — mechanical, verified by the existing serve-path tests plus one banner assertion.
本评论来自分诊座位 Routine。
Generated by Claude Code
claude commentedon Aug 17, 2026
Claim: PM loop round 1⚠️
Session:
session_012WKSnqAaoqtW3QX7SSf1VkBranch:
claude/issue-8978-serve-banner-artifact-rowWorktree:
objectstack-issue-8978Domain:
domain:cliFile surface:
packages/cli/src/commands/serve.ts,packages/cli/src/utils/format.ts(+ their sibling tests) (stop on breach; explain in the report)Container & model: S,
mode:subagent,model: sonnet—--tierreports no path-derived mandate; mechanical banner-row change, correctness decided by an assertion rather than by judgement, so the sonnet floor is the right tier. Clause ② not reachable from content (no accept/reject or public-surface change).Serial constraints cleared:
packages/cliis free this batch — the other two cards dispatched with it land inpackages/cloud-connection(#9070) andpackages/create-objectstack(#9263); no overlap.serve.tsis a three-way mutually-exclusive slot in this seat's hot-file queue (#8368 / #8985 / #8978) — #8985 is deliberately held back this round and must not be dispatched until this lands. No same-day churn: last touch of either file is445ae4d(2026-08-16).Premise re-verified on
origin/main@68cd4e7before dispatch:relativeConfigis derived atserve.ts:937and passed unconditionally asconfigFile: relativeConfigatserve.ts:3418— exactly the shape the card describes. The card is accurate.Generated by Claude Code
Generated by Claude Code
fix(cli): ready banner no longer names a config that was not read (#8978
chore: add changeset for #8978 serve banner fix
os-zhuang commentedon Aug 17, 2026
<!-- os-dev-report -->
{ "issue": 8978, "status": "done", "branch": "claude/issue-8978-serve-banner-artifact-row", "pr": "https://github.com/objectstack-ai/objectstack/pull/9347", "premise_still_valid": true, "summary": "serve's ready banner named a config file that was never read (OS_ARTIFACT_URL boot) or does not exist (plain artifact-fallback boot), because relativeConfig was passed to printServerReady unconditionally, derived before the artifact branch is decided. Added resolveBannerConfigRow (serve.ts) to decide the row: report the resolved artifact's already-redacted display on an OS_ARTIFACT_URL boot, omit the row on the other artifact-fallback paths (no safe display in hand), and report the config unchanged on the ordinary boot path. printServerReady (format.ts) now renders whichever of configFile/artifactSource it is given. Both files stayed within the declared file surface.", "tests": "Reverse verification (fix committed at e6e5d65d8, then `git checkout origin/main -- packages/cli/src/commands/serve.ts packages/cli/src/utils/format.ts` with tests untouched): `pnpm --filter '@objectstack/cli' exec vitest run src/commands/serve-banner-config-row.test.ts src/utils/format.config-artifact-row.test.ts` -> 7 of 8 failed (`TypeError: resolveBannerConfigRow is not a function`; and `expected 'Config: undefined' to be undefined`). Restored via `git checkout HEAD -- <paths>` -> same command -> 8/8 pass. Full suite after merging current origin/main and rebuilding: `pnpm --filter '@objectstack/cli' typecheck` clean; `pnpm --filter '@objectstack/cli' test -- --maxWorkers=2` -> 'Test Files 126 passed (126) / Tests 1375 passed (1375)' (one e2e failed ECONNREFUSED on the first run under load, isolated re-run passed -- environmental, unrelated to this diff). Gates: `pnpm check:cross-package-test-inputs` OK, `node scripts/check-cross-package-test-inputs.mjs` OK, `node scripts/docs-audit/check-affected-docs.mjs` OK (self-test; real mapper's affected-docs.mjs flags 2 non-release docs via an unrelated pre-existing `artifactSource` symbol -- MetadataPlugin's boot-mode config option, not this PR's new ServerReadyOptions field -- confirmed by grep, no doc drift). Convention-triggered (new test files added): `pnpm check:query-options-erasure` OK (67 unswept, none new), `pnpm check:type-check-coverage` OK (64/77 covered), `pnpm check:type-check-debt` OK after `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'` (33 ledger entries re-measured, 1926 raw errors, none above recorded), `pnpm check:engine-double-contract` OK (315 pinned, 133 DEBT, 2 exempt, none new), `pnpm check:where-matcher` OK (253 matchers, 0 silently-wrong, none new). All gates re-run on the merged tree at final HEAD 97db3f31e. `node scripts/check-nul-bytes.mjs` OK (self-scanned; caught and fixed a raw ESC control byte the Write tool materialized from a \\u001b escape in one new test file before it was committed).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
os-dev-report (marker retry — the HTML-comment form of the marker gets eaten by the GitHub body sanitizer even inside code fences; this comment leads with the literal text instead, per AGENTS.md 字节与sanitizer纪律)
{ "issue": 8978, "status": "done", "branch": "claude/issue-8978-serve-banner-artifact-row", "pr": "https://github.com/objectstack-ai/objectstack/pull/9347", "premise_still_valid": true, "summary": "serve's ready banner named a config file that was never read (OS_ARTIFACT_URL boot) or does not exist (plain artifact-fallback boot), because relativeConfig was passed to printServerReady unconditionally, derived before the artifact branch is decided. Added resolveBannerConfigRow (serve.ts) to decide the row: report the resolved artifact's already-redacted display on an OS_ARTIFACT_URL boot, omit the row on the other artifact-fallback paths (no safe display in hand), and report the config unchanged on the ordinary boot path. printServerReady (format.ts) now renders whichever of configFile/artifactSource it is given. Both files stayed within the declared file surface.", "tests": "Reverse verification (fix committed at e6e5d65d8, then `git checkout origin/main -- packages/cli/src/commands/serve.ts packages/cli/src/utils/format.ts` with tests untouched): `pnpm --filter '@objectstack/cli' exec vitest run src/commands/serve-banner-config-row.test.ts src/utils/format.config-artifact-row.test.ts` -> 7 of 8 failed (`TypeError: resolveBannerConfigRow is not a function`; and `expected 'Config: undefined' to be undefined`). Restored via `git checkout HEAD -- (paths)` -> same command -> 8/8 pass. Full suite after merging current origin/main and rebuilding: `pnpm --filter '@objectstack/cli' typecheck` clean; `pnpm --filter '@objectstack/cli' test -- --maxWorkers=2` -> 'Test Files 126 passed (126) / Tests 1375 passed (1375)' (one e2e failed ECONNREFUSED on the first run under load, isolated re-run passed -- environmental, unrelated to this diff). Gates: `pnpm check:cross-package-test-inputs` OK, `node scripts/check-cross-package-test-inputs.mjs` OK, `node scripts/docs-audit/check-affected-docs.mjs` OK (self-test; real mapper's affected-docs.mjs flags 2 non-release docs via an unrelated pre-existing `artifactSource` symbol -- MetadataPlugin's boot-mode config option, not this PR's new ServerReadyOptions field -- confirmed by grep, no doc drift). Convention-triggered (new test files added): `pnpm check:query-options-erasure` OK (67 unswept, none new), `pnpm check:type-check-coverage` OK (64/77 covered), `pnpm check:type-check-debt` OK after `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'` (33 ledger entries re-measured, 1926 raw errors, none above recorded), `pnpm check:engine-double-contract` OK (315 pinned, 133 DEBT, 2 exempt, none new), `pnpm check:where-matcher` OK (253 matchers, 0 silently-wrong, none new). All gates re-run on the merged tree at final HEAD 97db3f31e. `node scripts/check-nul-bytes.mjs` OK (self-scanned; caught and fixed a raw ESC control byte the Write tool materialized from a u001b escape in one new test file before it was committed).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Aug 17, 2026
ACCEPT — PR #9347
Reviewer of record:
domain:cliPM seat, sessionsession_012WKSnqAaoqtW3QX7SSf1Vk. Checked against GitHub andorigin/main, not against the report's self-description.Checklist: draft ✅ · base
main✅ · first lineFixes #8978✅ · 5 changed files, all inside the declared surface ✅ · changeset@objectstack/cli: patch✅ · path fork clean (nodocs/adr/**,.claude/skills/**,skills/**) ✅ · nopackages/spec/src/**, so no contract-review hold ✅ · all five required checks green, each read from its own jobconclusion—TypeScript Type Check,ESLint,Test Core(4/4),Dogfood Regression Gate(4/4),Build Core— ⛔ not from an aggregate ✅Spot readings — the two load-bearing claims were verified, not accepted
"The docs-drift rows come from a pre-existing
artifactSource, not my new field." True.artifactSourcealready exists onmain(serve.ts:1780,start.ts:202+) as MetadataPlugin's boot-mode config option, and both flagged pages document that (artifactSource: { mode: 'local-file', path: … }), not the newServerReadyOptionsfield. A symbol-name collision, correctly diagnosed. The release-owned page the bot also listed is untouched, as the guardrail requires.configFilewent from required to optional — does anything regress? Checked every caller rather than reasoning about it: exactly one production call site (serve.ts:3418); every other reference is a test that passesconfigFileexplicitly, so theConfig:row still renders for them. Nothing silently loses its row.Both defect paths are covered, which was the ruling.
resolveBannerConfigRowis a pure exported function with three branches, and the new pins assert all of them — including "never emits BOTH a configFile and an artifactSource", which pins the invariant rather than only the happy path. Reverse verification predicted the direction before running (7 of 8 red against unfixed code, 8/8 after).⭐ One judgement worth endorsing explicitly. On the plain artifact-fallback path the row is omitted rather than filled with
OS_ARTIFACT_PATH, because that value may itself carry credentials and no redacted display is in hand there. Printing it would have satisfied the card's literal suggestion and leaked secrets into an operator-facing banner. Choosing a missing row over a possibly-credentialed one is the right trade, and it is documented at the site.Deviations
os-dev-reportcomments on this card are entity-escaped, so the machine-readable marker is absent and collection fell back to reading content. The report itself is complete and accurate; only the marker is affected. HTML comments are stripped or escaped on write — anything that must survive belongs in visible markdown.check:nul-bytesbefore the commit. Self-caught, correctly handled.Check PR Size,Auto Label) from acodeload.github.com429 incident affecting third-party action downloads. Neither is on this PR, and neither is in the ruleset's required set.⇒ Flipping ready and enqueuing.
Generated by Claude Code
fix(cli): serve's ready banner no longer names a config that was not …