Found while verifying ADR-0110 D5's boot inventory (#4011). The inventory turned out to have its own placement bug (fixed there), but proving that exposed a bigger, older one: no plugin's boot-phase logger output reaches the console on either CLI entrypoint, at any log level.
Evidence
All on main (d6bfb3d), examples/app-todo, injected with a deliberately undeclared handler so a boot WARN is guaranteed to be emitted (verified below):
| Run |
Lines |
Plugin boot WARNs |
bootstrap / kernel:ready traces |
os dev (default level) |
31 |
0 |
0 |
os dev --log-level debug |
2,360 |
0 |
0 |
os serve objectstack.config.ts |
19 |
0 |
0 |
Programmatic kernel.bootstrap() with a plain logger |
— |
✅ present |
✅ present |
The debug run is the damning one: it happily streams thousands of data-phase kernel lines ("event":"beforeFind" × 2,524, "event":"afterUpdate" × 2,472 …) yet contains zero boot-phase lines — not Triggering kernel:ready hook (kernel.ts:363, a plain logger.debug), not ✅ Bootstrap complete, not one plugin logger.warn. So the logger sink is live and verbose — but only from some point after boot. Everything a plugin says during init/start/kernel:ready goes nowhere.
The two ⚠ lines that DO appear in the default output (⚠ Console dist not found…, ⚠ Flows: 4 flow(s) declared but…) are the CLI's own hand-rendered summary, not kernel logger output — which is presumably why the gap has been invisible: the summary looks like boot diagnostics exist.
Why this is a real defect and not cosmetics
What "fixed" looks like
Boot-phase logger.warn/logger.error from plugins reach the console on os dev and os serve at the default level, same as data-phase lines already do. Two plausible shapes:
- attach the console sink to the kernel logger before
kernel.bootstrap() instead of after; or
- buffer boot-phase records and flush them right after the CLI banner/summary.
(1) is presumably how it was always meant to work. A regression test can reuse #4011's positive-control recipe: boot an example with an injected undeclared handler through the CLI path and assert [action-governance] appears on stdout — that single assertion covers the whole class, because it fails whenever boot-phase WARNs are swallowed.
Relationship to v17
Not release-blocking on its own — nothing that previously surfaced regresses. But it hollows out two documented v17 stories (the D5 checklist; ADR-0032's warn-by-default) until fixed, so it is worth triaging right behind the release.
Found while verifying ADR-0110 D5's boot inventory (#4011). The inventory turned out to have its own placement bug (fixed there), but proving that exposed a bigger, older one: no plugin's boot-phase logger output reaches the console on either CLI entrypoint, at any log level.
Evidence
All on
main(d6bfb3d),examples/app-todo, injected with a deliberately undeclared handler so a boot WARN is guaranteed to be emitted (verified below):bootstrap/kernel:readytracesos dev(default level)os dev --log-level debugos serve objectstack.config.tskernel.bootstrap()with a plain loggerThe debug run is the damning one: it happily streams thousands of data-phase kernel lines (
"event":"beforeFind"× 2,524,"event":"afterUpdate"× 2,472 …) yet contains zero boot-phase lines — notTriggering kernel:ready hook(kernel.ts:363, a plainlogger.debug), not✅ Bootstrap complete, not one pluginlogger.warn. So the logger sink is live and verbose — but only from some point after boot. Everything a plugin says during init/start/kernel:ready goes nowhere.The two
⚠lines that DO appear in the default output (⚠ Console dist not found…,⚠ Flows: 4 flow(s) declared but…) are the CLI's own hand-rendered summary, not kernel logger output — which is presumably why the gap has been invisible: the summary looks like boot diagnostics exist.Why this is a real defect and not cosmetics
serve.ts's own--log-levelhelp text states the contract this breaks: "Defaults to …warnso flow/hook execution failures surface (ADR-0032)." A flow or hook that fails during boot-phase binding surfaces nowhere.[action-governance]inventory — the checklist that justifies D3's no-opt-out refusal (feat(runtime)!: the undeclared-action refusal has no opt-out — dropOS_ALLOW_UNDECLARED_ACTIONS(ADR-0110 D3 revised) #3987 removed the migration valve on exactly the argument that "the boot inventory names every offender") — is emitted correctly since fix(objectql,runtime): the ADR-0110 D5 inventory is engine-owned — AppPlugin never ran it underos dev#4011 but still invisible onos dev/os serve.console.warndirectly) is in the same hole. Plugins have visibly worked around this already — theDEGRADED BOOTdatasource warnings print via bareconsole.*, bypassing the logger, which is a tell that the sink has been quietly unusable for a while.What "fixed" looks like
Boot-phase
logger.warn/logger.errorfrom plugins reach the console onos devandos serveat the default level, same as data-phase lines already do. Two plausible shapes:kernel.bootstrap()instead of after; or(1) is presumably how it was always meant to work. A regression test can reuse #4011's positive-control recipe: boot an example with an injected undeclared handler through the CLI path and assert
[action-governance]appears on stdout — that single assertion covers the whole class, because it fails whenever boot-phase WARNs are swallowed.Relationship to v17
Not release-blocking on its own — nothing that previously surfaced regresses. But it hollows out two documented v17 stories (the D5 checklist; ADR-0032's warn-by-default) until fixed, so it is worth triaging right behind the release.