Skip to content

fix(cli): a serve boot that binds no HTTP server announces no address, and MCP: prints only when its route is mounted - #23947

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23743-no-server-announcements
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23743-no-server-announcements

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23743
Clause-②: no

What this changes

This is the remainder of #23623 / PR #23741. That PR gated the ready banner's address rows on a composed HTTP server. It left two things as they were, and this PR fixes both.

1. The machine channels on a boot that binds no HTTP server. os serve --no-server, with no transport composed, sent { type: 'objectstack:listening', port, url } and wrote runtime.ENV.PROJECT.json with the same port and url. Nothing listened on that port. The docs row (content/docs/deployment/cli.mdx) says: "objectstack:listening | os serve | The HTTP server is bound."

  • packages/cli/src/commands/serve.ts, the publish site. publishBoundPort(boundPort, runtimeBoundPortChannels(printBanner), boundProtocol) now runs only when httpServer === 'composed'. Otherwise only the banner prints. httpServer is the resolveHttpServerComposition(kernel, flags.server) value the banner's address rows already read. One predicate, read once, decides all three channels, so the banner, the IPC message and the state file cannot disagree about whether there is an address. publishBoundPort, its three channels and their order are unchanged. resolveBoundPort is unchanged too: it still falls back to the requested port, and the publish site no longer publishes that fallback.
  • objectstack:seed-settled on that boot: still sent, and it is the only message. Consumers measured in this repository: the os dev parent (dev.ts, read, not edited) relays it and never spawns a --no-server child, because os dev has no server flag. The docs example waits for seed-settled alone. SeedSettledMessage's own rule is that a waiting parent must never hang. A kernel-only boot still seeds, so dropping the message would hang exactly the parent that rule protects. Both docs rows are updated together. The listening row now says the message is not sent by a boot that binds no HTTP server. The seed-settled row keeps "always after objectstack:listening" and adds that on such a boot it is still sent and is the only message.
  • The runtime state file on that boot: not written. Readers measured in this repository: scripts/publish-smoke.sh (reads .port, then curls /api/v1/health; it spawns objectstack dev, which never runs --no-server), plus the CLI's own tests (serve-bound-port-publication.test.ts, test/serve-bound-port-publish-order.test.ts, test/serve-publishes-bound-port.e2e.test.ts, test/serve-runtime-state-project-key.test.ts, test/helpers/runtime-state-child.ts). No docs page states a contract for the file. I chose "not written" over "written with no URL or an unbound marker" for three reasons. The file's whole payload answers "is my server running, and where?", and runtimeStateFileName's docblock already calls a missing file "a loud, correct answer". An unbound record would add a second payload shape that every reader would have to handle; publish-smoke.sh's jq '.port | numbers' would read it as "no port yet" and probe until its timeout. And the payload stays the shape the project-key ruling (serve.ts, projectStateKey) chose not to widen.
  • os start never passes --no-server, and os dev never does either, so every in-repo consumer of these two channels sees the same bytes as before.

2. The banner's MCP: row under a config-mounted server. Under --no-server, with a config that mounts its own HonoServerPlugin, the banner printed MCP: http://localhost:PORT/api/v1/mcp and its connect hint, but GET /api/v1/mcp answered 404. The plugin that mounts that route is the runtime dispatcher, createDispatcherPlugin, and serve composes it only inside if (flags.server).

  • serve.ts: readComposedMcpRouteOwner(kernel) asks the booted kernel hasPlugin('com.objectstack.runtime.dispatcher'). This is a kernel read like readComposedHttpServer, so a config that mounts its own dispatcher is answered as well. The dispatcher registers no service; it only consumes http.server. So the kernel's plugin registry is the read, the same one HonoServerPlugin already asks before it warns that no data or discovery API is mounted. The banner gets mcpEnabled: isMcpServerEnabled() && mcpRouteMounted, where mcpRouteMounted = httpServer === 'composed' && readComposedMcpRouteOwner(kernel). If the kernel cannot answer, the result is false.
  • packages/cli/src/utils/format.ts: doc comment only, on the mcpEnabled field the MCP: row reads, saying what serve now passes. printServerReady is unchanged.

⛔ Not changed: exports of @objectstack/cli, flags, exit codes, process lifetime, packages/spec, packages/runtime, dev.ts.

Measured at the door

Each boot runs node packages/cli/bin/run.js serve objectstack.config.ts --port PORT ARGS on the built CLI, spawned with an ipc channel. NODE_ENV is unset except under --dev, and OS_CLOUD_URL=off. The fixture is a throwaway host config with one object and one plugin instance. Before is 7a6ba03109, after is cccb00ea51. "State file" means OS_HOME/runtime.env_local.PROJECT.json as read when the listening message arrived.

Shape IPC before IPC after State file before / after Banner address rows after Probe (before and after)
production --no-server --preset minimal (the card's command) objectstack:listening with url: 'http://localhost:38393', then seed-settled seed-settled only written with that url / none No HTTP server: … line (unchanged from #23623) ECONNREFUSED on /, /api/v1/health, /api/v1/mcp
--dev --no-server listening, then seed-settled seed-settled only written / none the same line ECONNREFUSED
production default (control) listening with the bound port, then seed-settled unchanged written / written, same port and url API:, MCP: and the hint (unchanged) /api/v1/health 200, /api/v1/mcp 401
--no-server --preset minimal + config-mounted HonoServerPlugin listening, then seed-settled unchanged: a transport is bound written / written API: only; before: API:, MCP: and the hint /api/v1/mcp 404, /api/v1/data/fx_task 401
default preset + config-mounted HonoServerPlugin, --server (control) not measured before listening, then seed-settled — / written API:, MCP: and the hint /api/v1/health 200, /api/v1/mcp 401

Tests

  • src/commands/serve-no-transport-announcements.test.ts (new, unit, 10 cases).
    • readComposedMcpRouteOwner against a real ObjectKernel: true with createDispatcherPlugin() registered, and false with a transport-only composition.
    • MCP_ROUTE_OWNER_PLUGIN is read off createDispatcherPlugin().name, so a rename of the dispatcher reds here instead of silently dropping the row.
    • It asks for exactly that name, and answers false for no kernel, no hasPlugin, a throwing hasPlugin, and a non-boolean answer.
    • The run() wiring, with comments masked by scripts/js-comment-mask.mjs: the publish call sits in the httpServer === 'composed' arm with printBanner() alone in the other arm; httpServer is resolved once and is what the banner reads; the mcpEnabled / mcpRouteMounted spelling is pinned; the settle latch is released once, after the gated publication on both arms. An anti-vacuity case checks that the mask blanked comments.
  • src/commands/serve-no-transport-announcements.integration.test.ts (new). vitest-tiers.ts puts it in the integration tier. It runs three boots of the built CLI, each with an ipc channel and an OS_HOME directory watched from before the spawn.
    • Production --no-server: no objectstack:listening; the messages are exactly ['objectstack:seed-settled']; and no runtime.*.json was ever created. The watch matters: this shape exits on its own and its exit handler deletes the file, so a listing taken afterwards cannot tell "never written" from "cleaned up".
    • Control, default boot: listening and then seed-settled; the message equals { port, url } of the API: row; the state file present at that moment carries the same port and url; an MCP: row prints; health answers 200 and /api/v1/mcp answers something other than 404.
    • Config-mounted Hono under --no-server: no MCP: row and no connect hint; /api/v1/mcp answers 404 (the reason the row must not print); the IPC message and the state file both carry the API: address.
    • The three no-transport assertions are expect.soft, so each channel reports on its own. The file declares // test-budget: 30: the three local runs took 19.4 s, 18.9 s and 20.9 s on a shared container; 20.9 × 1.25, rounded up.
  • Unit tier, pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 --shard=K/4. It is sharded because one unsharded run does not fit the container's foreground cap. Shards 1 to 3 ran on 23b3033ffc (this branch merged with origin/main); shard 4 also ran there, and its one failure was fixed in cccb00ea51.
    • shard 1: 76 files, 1418 tests passed.
    • shard 2: 76 files, 1099 passed and 3 failed. All 3 were Test timed out in 5000ms while my unlocked gate battery shared the cores. The two files (src/utils/boot-preparation-parity.test.ts, test/hook-timeout-override-refusal.test.ts) re-run alone: 2 files, 41 tests passed.
    • shard 3: 76 files, 918 passed.
    • shard 4: 75 files, 1071 passed and 1 failed. serve-observability-registration.test.ts finds the transport by the first raw-source occurrence of the HonoServerPlugin constructor call, and my new docblock spelled that call above the registration. cccb00ea51 rewords the docblock.
  • On the final head cccb00ea51, after rebuilding the CLI:
    • six unit files (serve-observability-registration, the new unit file, serve-bound-port-publication, serve-http-server-composition, format.server-ready-no-http-server, test/serve-bound-port-publish-order): 6 files, 82 tests passed;
    • integration, scoped (the diff adds an integration file and edits serve.ts): the new file, serve-no-server-banner.integration, test/serve-no-storage-boot-guard.integration, test/dev-boot-output-order.integration: 4 files, 15 tests passed;
    • OS_TEST_TIERS=nightly, test/serve-publishes-bound-port.e2e.test.ts (the publication's own end-to-end pin, which runs nightly and not on this PR): 1 file, 6 tests passed;
    • the rest of the integration tier is declared to CI.

Ablation

The fix was committed first. Each leg went through node scripts/ablation-replace.mjs in wrap mode (it restores on exit, and a second trap restores from HEAD). Then the CLI was rebuilt, node scripts/ablation-dist-preflight.mjs @objectstack/cli MARKER was run, and the two new files were run.

  • The publish gate. The anchor if (httpServer === 'composed') { was replaced with an always-true condition carrying the marker ablation-23743-gate.
    • On disk: anchor x1 to x0, marker x0 to x1, blob 8de5619d0a58 to f04d083f4255. The marker was present in 1 built file (dist/commands/serve.js).
    • Result: Tests 3 failed | 10 passed (13). Integration case 1 reported all three soft assertions: "objectstack:listening was sent by a boot that bound no HTTP server", "seed-settled is not the boot's one message", "a runtime state file was written by a boot that bound no HTTP server". So the watched-directory read does see a file that the exit handler later deletes. The two wiring source pins also failed. Both controls passed.
    • Direction: turned red, as predicted.
    • Restore: blob back to 8de5619d0a58, which is HEAD; git diff HEAD empty; git status --porcelain empty. After a rebuild, --absent found the marker in none of the 640 built files. Tests 13 passed (13).
  • The MCP: gate. mcpEnabled: isMcpServerEnabled() && mcpRouteMounted, was replaced with an always-true right operand carrying ablation-23743-mcp.
    • On disk: anchor x1 to x0, blob 8de5619d0a58 to 630307660e27; the marker was present in 1 built file.
    • Result: Tests 2 failed | 11 passed (13): "an MCP: row printed for an unmounted route" (Hono case) and the mcpEnabled source pin. The no-transport case and the default control passed.
    • Direction: turned red, as predicted.
    • Restore: proven the same way; --absent in all 640 files; Tests 13 passed (13).

The ablations ran against the serve.ts blob 8de5619d0a58, which is unchanged from 99c982b4cd through 23b3033ffc. cccb00ea51 changes only a docblock in it.

Gates

All on the final head cccb00ea51.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derives 96 commands. Its stderr names the tree as objectstack-ai/objectstack at cccb00ea51, with 6 paths changed against merge base 944f03850. Each command was run as cmd > log 2>&1; ec=$?, and all 96 exited 0.
    • On the first pass (on 23b3033ffc), three gates exited 3 with PREREQUISITE NOT MET: spec check:skill-examples, check:dual-build-cjs-loads and check:i18n-coverage. The packages they read had no dist/: studio, client-react, connector-slack, the example apps and others outside the CLI's closure. After turbo run build of those (75 tasks, exit 0), all three exit 0 on cccb00ea51.
    • --ran ran.list prints: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED (a DERIVED zero — all 96 recorded an exit code and none of them is 3).
  • pnpm --filter @objectstack/cli typecheck exits 0: tsc --noEmit, then check:test-typecheck: OK. tsc -p tsconfig.json --noEmit --listFilesOnly lists both new files (2 of 2).
  • Lint lane: pnpm exec eslint --no-inline-config over the four changed .ts files exits 0 with no output. The repo-wide pnpm lint is CI's.
  • Not run locally, by rule: check:dev-prereqs and check:skill-frame-freshness (no packages/spec build or frame file is touched), and check-platform-checklist.mjs --changed-from (no exported symbol is renamed or removed; two are added).
  • CI check-runs: not read yet; they start when this PR opens.

Acceptance notes

  • Process lifetime (cli/serve: --no-server --preset minimal prints the ready banner ending "Press Ctrl+C to stop" and then exits 0 on its own about 40 ms later, removing the runtime state file it had just written #23744, not touched here). Measured on base 7a6ba03109 and after cccb00ea51: production --no-server --preset minimal prints its banner about 5.1 s to 5.8 s after spawn, and then exits 0 by itself within milliseconds, with or without an ipc channel. seed-settled arrives before that exit in every run measured. --dev --no-server stays up until stopped. This PR changes neither.
  • A stale state file is no longer overwritten by a no-transport boot. If an earlier boot of the same project died without running its exit handler (SIGKILL, a crash), its runtime.ENV.PROJECT.json survives. Before, a later --no-server boot overwrote it with its own record, carrying a URL nothing listened on. Now that boot writes nothing, so the stale record stays, naming the dead pid. A reader that checks pid liveness is unaffected. The file is documented as best-effort and was never cleaned up on a hard kill. Observation only; no carrier.
  • start.ts comment. A comment in packages/cli/src/commands/start.ts says serve publishes objectstack:listening "unconditionally". It is now conditional on a composed transport. os start never passes --no-server, so for its child the sentence still holds in practice. It is outside this card's file surface and is left as is. Observation only.
  • Dispatcher prefix. A config that mounts its own createDispatcherPlugin({ prefix }) with a non-default prefix gets an MCP: row naming /api/v1/mcp whatever the prefix. serve's own dispatcher always uses the default. No in-repo producer of that shape was found, and the behaviour is unchanged by this PR. Observation only.
  • readComposedMcpRouteOwner answers "is the route's owner composed", not "is the route mounted". The dispatcher mounts /api/v1/mcp unconditionally in its start() once an http.server is registered, and the row also requires a composed transport. So with today's dispatcher the two are the same. No live route table exists on IHttpServer to read instead.

Generated by Claude Code

…, and MCP: prints only when its route is mounted

On a boot with no HTTP transport composed (`--no-server` and no
config-mounted transport), `os serve` no longer sends
`objectstack:listening` and no longer writes
`runtime.<environment>.<project>.json`: both used to carry the requested
port, which nothing listened on. The banner still prints.
`objectstack:seed-settled` is still sent and is then the boot's only
message. The gate is the same `httpServer` composition the banner's
address rows read.

The banner's `MCP:` row and its connect hint now also require the route's
owner, the runtime dispatcher, to be composed (`kernel.hasPlugin`), so a
config-mounted `HonoServerPlugin` under `--no-server` no longer advertises
`/api/v1/mcp`, which answers 404 there.

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
A unit file drives readComposedMcpRouteOwner against a real ObjectKernel
and pins the run() wiring (comments masked); an integration file boots the
built CLI three times with an ipc channel and a watched OS_HOME.

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
…tegration budget reading

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
… budget from three local runs

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
…needle

serve-observability-registration.test.ts finds the transport by the first
occurrence of the HonoServerPlugin constructor call in serve.ts's raw source;
the new docblock spelled that call and sat above the registration.

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 6 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line), /api/v1/mcp/skill (route, a path literal in a comment on a changed line))
  • content/docs/ai/connect-mcp.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line), /api/v1/mcp/skill (route, a path literal in a comment on a changed line))
  • content/docs/ai/index.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/ai/natural-language-queries.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/api/index.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/capabilities/ai.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/deployment/cli.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/deployment/environment-variables.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/deployment/index.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/deployment/self-hosting.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/getting-started/build-with-claude-code.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/permissions/authorization.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/releases/v16.mdx (via /api/v1/mcp (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: os serve (command, 33 pages), /api/v1/data (route, 40 pages)
  • 1 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 — 29 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 11fea6b000894eb6c4cf6d79f28fe3109cca46bb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 789330410aa4c163d19862dbf89d97d83444b44b — the merge of head cccb00ea51453ea8cc05e2d98bf457ec39cd466b into base 11fea6b000894eb6c4cf6d79f28fe3109cca46bb, 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 789330410aa4c163d19862dbf89d97d83444b44b && git checkout 789330410aa4c163d19862dbf89d97d83444b44b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 11fea6b000894eb6c4cf6d79f28fe3109cca46bb cccb00ea51453ea8cc05e2d98bf457ec39cd466b && git checkout -B drift-repro 11fea6b000894eb6c4cf6d79f28fe3109cca46bb && git merge --no-ff cccb00ea51453ea8cc05e2d98bf457ec39cd466b

node scripts/docs-audit/affected-docs.mjs --json 11fea6b000894eb6c4cf6d79f28fe3109cca46bb

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 11fea6b000894eb6c4cf6d79f28fe3109cca46bb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

✅ ACCEPT: PR #23947 at cccb00ea51 (#23743, Fixes #23743). It queues on a true-green head

domain:cli seat 1 (#6024) · marchtian · session session_01PFB9ZGRWYvjhTXsd8i6KBp · 2026-10-16T13:53Z · holder of claim 6213641477. Report: os-dev-report 6216644056.

席位核对

  1. Shape, read on GitHub: draft against main, head cccb00ea51. Line 1 Fixes #23743, the only closing keyword. Line 2 Clause-②: no, line-initial and bare, as the claim declares. Assignee marchtian. 6 files: serve.ts, format.ts (docblock only), content/docs/deployment/cli.mdx (the two IPC rows), two new test files, one changeset. check-governed-merges --pr 23947: NOT governed (0 of 6 paths). The non-merge commits carry the model-free trailer pair.
  2. The change, read against the diff: at the publish site publishBoundPort(...) runs only when httpServer === 'composed', the same resolveHttpServerComposition value the banner's address rows already read; otherwise only the banner prints. One predicate decides the banner rows, objectstack:listening and the runtime state file. objectstack:seed-settled is still sent on a no-transport boot and is its only message, which keeps SeedSettledMessage's no-hang rule. The MCP: row now needs mcpRouteMounted (a composed transport and kernel.hasPlugin(MCP_ROUTE_OWNER_PLUGIN), the dispatcher that mounts /api/v1/mcp). publishBoundPort, its channels and their order, resolveBoundPort and printServerReady are unchanged. No new package-entry export; no packages/spec edit.
  3. Docs: cli.mdx:269-:270 now say that a boot binding no HTTP server sends no objectstack:listening and that seed-settled is then its only message. Both match the code. Nothing under content/docs/releases/.
  4. Pins and measurement: both halves reproduced at the public door on base 7a6ba03109 before any edit (a listening message and state file naming a port answering ECONNREFUSED; an MCP: row for a route answering 404). The integration file boots bin/run.js three times with an ipc channel: production --no-server (messages exactly ['objectstack:seed-settled'], no state file ever created), the default-boot CONTROL ([listening, seed-settled], the message equal to the API: row, health 200), and a config-mounted Hono under --no-server (no MCP: row, /mcp 404). Ablations through ablation-replace.mjs with a trap restore and a rebuilt CLI: the publish gate (3 soft legs and both wiring pins red, controls green) and the MCP gate (the Hono case and its source pin red); both restored to the HEAD blob. They ran on blob 8de5619d0a58, before the docblock-only cccb00ea51.
  5. CI at cccb00ea51: 41 check-runs, 36 success, 5 skipped (each on the EXPECTED_SKIPS roster); the five required are success, the six Test Core shards and Build Docs included. mergeable_state clean; git merge-tree against current origin/main is clean.
  6. The changeset (@objectstack/cli patch): what an operator now sees on --no-server (no listening message, no state file, no MCP: row for an unmounted route) and what is unchanged (seed-settled) match the diff.
  7. Out of scope: out_of_scope_findings[0] is carried to cli/serve: --no-server --preset minimal prints the ready banner ending "Press Ctrl+C to stop" and then exits 0 on its own about 40 ms later, removing the runtime state file it had just written #23744, which asks exactly that question (the production --no-server --preset minimal process exits 0 by itself after its banner). The seat posts the measurement there at this PR's landing, since two of the channels it names change here. [1] (a stale state file no longer overwritten, the start.ts comment, a non-default dispatcher prefix): Acceptance notes, no class, carrier none.

Landing: this seat flips ready and arms auto-merge through the relay.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 13:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 13:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 161aee1 Oct 16, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23743-no-server-announcements branch October 16, 2026 14:17
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/l tests tooling

Projects

None yet

2 participants