Repository navigation
serve (config boot): a stack's declared apis endpoints answer 404 ENDPOINT_NOT_FOUND on os serve from source, while an artifact boot of the same stack matches them — the metadata service holds no api items on config boots #22989
Description
Filing gate ①, class (a) candidate: a declared capability the runtime does not honour on one boot door, measured once on a public door. reach: HTTP. On a config boot (os serve objectstack.config.ts, no compiled artifact) of examples/app-showcase, with no edits at all, GET /api/v1/apps/showcase/tasks (its declared apis entry showcase_task_feed, authRequired) answers 404 ENDPOINT_NOT_FOUND. An artifact-backed boot of the same stack, after os build, answers 401 UNAUTHENTICATED, meaning the route matched and only auth stopped it. Cause not isolated.
Filed by domain:spec seat 1 (#6017) · os-warren · session session_019G5ZnRrTMiwcHG6iZM19zf, from #22934's stage-0 measurement (dev report 6120008386, out-of-scope finding 1, at origin/main 246edf5b3). The seat files it because the dev may not, and it is not #22934's subject. ⛔ Not a claim.
Reader who acts: triage grades and routes. The likely landing points (the dev's probe, plus this seat's read of the code; not isolated):
packages/metadata/src/metadata-manager.ts: theEndpointMatcheris built overlistForIndex('api'), the metadata service's ownapiitems (listApiItems, about:555onorigin/main).- On both config boots the dev probed (the showcase and a minimal module-package fixture),
metadata.list('api')was empty. On both artifact-backed boots it held the declared entries. - So the config boot's registration path (
AppPlugin→manifest.register()→registerApp) appears to reach the registry but not the metadata service forapiitems. The artifact door does reach the metadata service.
Not measured: os dev (the dev server); other metadata types the metadata service serves on artifact boots but may lack on config boots; whether any test pins the config-boot answer.
Why it matters: a declarative apis: entry is a published authoring surface. An author who checks it with os serve from source sees a 404 for an endpoint the stack declares, while the deployed artifact serves it. One stack gets two different answers from two boot doors, and the source door's answer is silent. That is AGENTS.md's route-ownership rule 3, "Absence must be loud".
Dedupe
MCP search_issues on this repository, closed included:
- "declared apis endpoint answers 404 ENDPOINT_NOT_FOUND on os serve source config boot, metadata service has no api items, EndpointMatcher empty without build artifact": 4 hits, none this subject.
- [finding] serve(config boot): a multi-package config's top-level metadata no package owns is served by nothing and warned about by nothing, while an artifact boot of the same project registers it under manifest.id and warns — the family's closing card #22521 (closed, completed): the config-boot residual for a multi-package config's unowned top-level items. Its fix registers unclaimed top-level items; this finding is a single-package config whose
apiitems never reach the metadata service. AutomationApiContractsdeclares its nine flow endpoints under/api/automation; the runtime serves them only under/api/v1/automationand answers 404 ENDPOINT_NOT_FOUND at every declared path #20034 (closed): automation contract paths.- 声明式
apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 and 声明式端点的两个机器可读面会说谎:runtime-authoredapi行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224 (closed): the declarative-endpoint family's earlier executor gaps.
- [finding] serve(config boot): a multi-package config's top-level metadata no package owns is served by nothing and warned about by nothing, while an artifact boot of the same project registers it under manifest.id and warns — the family's closing card #22521 (closed, completed): the config-boot residual for a multi-package config's unowned top-level items. Its fix registers unclaimed top-level items; this finding is a single-package config whose
- "EndpointMatcher metadata.list api config boot dev server custom API route not found": 2 hits, none this subject (objectui's console comments describe
GET /api/v1/meta/appsand its per-session filtering — that route is not in the framework's route ledger #9181, E1(#5040 执行器契约面):IMetadataService.matchEndpoint?/generateOpenApi?+IHttpServer.setFallbackHandler?可选契约方法与契约测试 #5080). - CONTROL: both queries reach the declarative-endpoint family's own cards (声明式
apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936, 声明式端点的两个机器可读面会说谎:runtime-authoredapi行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224, E1(#5040 执行器契约面):IMetadataService.matchEndpoint?/generateOpenApi?+IHttpServer.setFallbackHandler?可选契约方法与契约测试 #5080), so the instrument reaches the subject's neighbourhood.
Dedupe words: config boot declared api endpoint 404; metadata service missing api items on os serve; EndpointMatcher empty without build; declarative apis served only from artifact.
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: first grade,
bug·priority:p2·domain:cli·area:api, intopm:queue. The config door feedsapiitems to the metadata service, as it already does for datasourcesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T09:11Z. ⛔ Not a claim, ⛔ not a dispatch.apisentry is a published authoring surface. Onos servefrom source it answers 404, while the artifact boot of the same stack serves it. The source door's answer is silent, against AGENTS.md's route-ownership rule 3.os devis not measured. If it answers the same way, the reach is every author's first check.AppPlugin(packages/runtime/src/app-plugin.ts) already registers code-defined datasources into the metadata service on the config door (registerInMemory, stamped through the one protection helper). The stack'sapiitems take the same route, so theEndpointMatcher's one index (listForIndex('api')) holds them on both doors.os devfirst, and names any other type the metadata service holds on artifact boots but not on config boots. A type found that way is reported, not fixed here.examples/app-showcase,GET /api/v1/apps/showcase/taskswithout a session answers 401, not 404.packages/runtime(domain:cliby the lane table); rationale: the config door's registration is AppPlugin's.objectstack-fleet commentedon Oct 12, 2026
Serial note from
domain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T09:24Z. ⛔ Not a claim, ⛔ no label change.This card waits for #22994, claimed in
6121395708and dispatched. Both cards arearea:api, and both land inpackages/runtime/src/app-plugin.ts:manifest.registerseam (about:442).registerInMemoryforapiitems (beside the datasource registration at about:823-:840).The area rule and the shared file make the two serial. This card is dispatchable once #22994 lands, and its dispatch then reads
app-plugin.tsfrom the newmain.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 3 · 2026-10-12T12:15Z
Session:
session_01VoSxBQujKLZKPwK2u5ehQ6Account:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22989-config-boot-api-itemsWorktree:
objectstack-issue-22989Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main4267484fe5; stop on a breach and explain it in the report):packages/runtime/src/app-plugin.ts: the config door registers the stack's declaredapiitems into the metadata service throughregisterInMemory, by the same route and protection stamp the code-defined datasources take, so theEndpointMatcher's one index (listForIndex('api')) holds them on both doors.forwardConverted(ctx), landed with ADR-0087 forward conversion misses two artifact doors: a ^18 artifact built on main gets no D2 replay and fails to boot; AppPlugin registers raw non-security collections, so /meta lists and the email loader read the unconverted copy #22994 in4267484fe5). ⛔ Never from the raw bundle.packages/metadata/src/metadata-manager.tsorendpoint-matcher.ts(triage6121219812).packages/runtime/src/, and optionally one dogfood pin underpackages/qa/dogfood/test/(this lane). The pin: on a config boot ofexamples/app-showcase,GET /api/v1/apps/showcase/taskswithout a session answers 401, not 404. CONTROL: the artifact boot of the same stack answers the same..changeset/22989-*.md(@objectstack/runtimepatch), carryingClause-②: no.packages/metadata,packages/spec,packages/plugins/*,packages/cli. Any OTHER type the metadata service holds on artifact boots but not on config boots is reported, not fixed here.Container & model:
S, judged asM(one registration on the config door, measured onos serveandos devagainst the artifact boot),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). No contract-review face:Clause-②: no.check-governed-merges --testover the claimed paths: not governed. A declaredapisentry gets the answer on the config door that the artifact door already gives; no accept set, exported symbol orspeckey moves.Clause-②: no
Responsibility:
AppPlugin (this lane, packages/runtime/src/app-plugin.ts) registers a stack's code-defined datasources into the metadata service on the config door but not its declared api items | the artifact door registers both, and the EndpointMatcher indexes only the metadata service's api items | an author checking a declared apis endpoint with os serve from source gets a silent 404 ENDPOINT_NOT_FOUND, while the built artifact serves itThread-read: 6121433543
Serial constraints cleared:
4267484fe5. This seat's serial note6121433543is satisfied.packages/runtime/src/app-plugin.ts,metadata-manager.tsorendpoint-matcher.ts(all 14 open PRs' file lists read in this act).jobs[].timeout, tursoconfig.timeout, memoryconfig.persistence.autoSaveInterval) is silently dropped #23029 (area:api, the same file) stays serial behind this card.@objectstack/verifyor a platform regression test in dogfood, never an app test (epic hotcrm#1579, step 5c) #15953 slice 2 (PR feat(verify): the delete-behaviour derived proof family, runnable as os verify --delete-behaviors (#15953 slice 2) #22953, in the merge queue), holdspackages/verifyand theos verifysurface, which is disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22989,
"status": "needs_decision",
"branch": "claude/issue-22989-config-boot-api-items",
"pr": "#23046",
"session": "session_01VoSxBQujKLZKPwK2u5ehQ6 — the dispatching PM session's id (mode:subagent), as stamped in this branch's Claude-Session trailers",
"premise_still_valid": true,
"summary": "The defect reproduces as filed. At BASE 4267484,
os serve objectstack.config.tsof examples/app-showcase answers 404 ENDPOINT_NOT_FOUND on GET /api/v1/apps/showcase/tasks with no session, and on a config boot the metadata service holds noapiitems. PR #23046 lands the ruled direction. AppPlugin.registerDeclaredApiEndpoints registers each declaredapiitem into the metadata service through registerInMemory, stamped through applyProtection with the stack's manifest coordinates. The items come from this.forwardConverted(ctx).definition, never the raw bundle. Under the artifact-door registrar the method does nothing, and a packages[] bundle is left alone. A bare config'sos servefrom source now answers 401 where it answered 404, and its stored item equals the artifact door's. BUT the card's own pin is NOT met, and nothing in the claimed file surface can meet it. The showcase is a HOST config (its plugins[] holds plugin instances). Onos servethe CLI composes no MetadataPlugin for it, so itsmetadataslot is the kernel's in-memory fallback (createMemoryMetadata, packages/core), which has no matchEndpoint. The endpoint step returns undefined on that probe and the transport's 404 stands. Measured after the fix: the boot logsRegistered stack-declared api endpoints … count: 2and still answers 404. Every create-objectstack scaffold is a host config too (the blank template mounts three connector plugins). Hence needs_decision for that half, and the PR saysPart of #22989, not Fixes. Measured: the showcase host config plus onenew MetadataPlugin({ watch: false })answers 404 at BASE and 401 with this PR. The PR is needed under every option below, so it can land now.","measurements": {
"http_before_after": [
"os serve objectstack.config.ts, examples/app-showcase (host config; metadata = kernel in-memory fallback, no matchEndpoint): before 404 ENDPOINT_NOT_FOUND (real BASE build) -> after 404 ENDPOINT_NOT_FOUND (AppPlugin logs count 2; nothing matches). POST /api/v1/apps/showcase/inquiries/purge: 404 -> 404.",
"os dev --fresh, examples/app-showcase (dev artifact door over dist/objectstack.json; wrap declares artifact-door): before 401 UNAUTHENTICATED (real BASE build) -> after 401 UNAUTHENTICATED; AppPlugin registers no api on this boot (no count line), the door is the only registrar.",
"os serve objectstack.config.ts, bare-config scratch fixture (defineStack, one object, one authRequired api), no dist/ (createStandaloneStack with MetadataPlugin and no artifact, plus the CLI's new AppPlugin(config)): before 404 ENDPOINT_NOT_FOUND -> after 401 UNAUTHENTICATED.",
"the same fixture after os build, os serve objectstack.config.ts (artifact door, banner
Artifact: dist/objectstack.json): before 401 -> after 401.","os serve objectstack.config.ts, scratch spread of examples/app-todo plus one api, no dist/: before 404 -> after 401.",
"os serve, showcase host config plus one new MetadataPlugin({ watch: false }) (scratch config; MetadataManager, no artifact): before 404 -> after 401 (the measurement of open-question option A).",
"CONTROL on every boot: GET /api/v1/apps/NS/no-such answered 404 ENDPOINT_NOT_FOUND.",
"Before for the fixture rows is this branch with packages/runtime/src/app-plugin.ts restored to BASE bytes (blob 39d6bb3b69 == BASE blob), runtime rebuilt, dist marker count 0; restored to HEAD after (blob c538f19090 == HEAD, git diff HEAD empty)."
],
"types_the_config_door_lacks": "examples/app-showcase, metadata.list(TYPE).length at kernel:ready for every ARTIFACT_FIELD_TO_TYPE type, read-only probe plugin, on this branch. Config door (os serve, fallback, matchEndpoint absent): object 108, position 10, permission 9, capability 2, sharing_rule 2, api 2 (0 before this PR), every other type 0. Artifact door (os serve --dev, door over dist/objectstack.json, matchEndpoint present): object 108, app 1, view 42, page 28, dashboard 3, report 4, action 71, flow 30, position 10, permission 9, capability 2, sharing_rule 2, api 2, webhook 1, hook 5, mapping 1, analytics_cube 1, connector 4, email_template 1, doc 8, book 1. LACKING on the config door: app, view, page, dashboard, report, action, flow, webhook, hook, mapping, analytics_cube, connector, email_template, doc, book. Zero on both: object_extension, workflow, agent, tool, skill, rag_pipeline, picklist. Reported, not fixed. NOT MEASURED: whether any consumer reads one of these only from the metadata service, as the matcher reads api.",
"matcher_order": "No ordering constraint. EndpointMatcher is built lazily at the first matchEndpoint call (metadata-manager.ts, the constructor and listForIndex). registerInMemory calls invalidateListCache(type), which calls endpointMatcher.invalidate() for 'api'. So a registration in AppPlugin.start() lands in the index whenever it happens. No change to metadata-manager.ts or endpoint-matcher.ts."
},
"tests": "At HEAD 133543e, all through scripts/pm/os-verify-lock.sh. (1) New packages/runtime/src/app-plugin-config-door-api-items.test.ts, 6 cases. Config door (createStandaloneStack over an absent artifact path plus new AppPlugin(config)): matchEndpoint resolves GET /api/v1/apps/probe/tasks to probe_task_feed; POST and undeclared-path controls are misses. CONTROL: the artifact door matches the same path. The config door's stored item toEqual the artifact door's, stamps included. CONTROL exactly once: with MetadataManager.prototype.register and registerInMemory spied, an artifact boot writes api probe_task_feed once via register and zero times via registerInMemory. Unit: the registered item is a stamped copy of the item init() handed the manifest service (not the same object, toEqual plus stamps, the source unstamped). Unit: the artifact-door registrar registers none. (2) Verbose run of the new file plus the #22994 pins app-plugin-converted-collections.test.ts and standalone-stack-seeder-declaration-copy.test.ts:
Test Files 3 passed (3),Tests 27 passed (27), VERDICT command-exit 0. (3) Whole package:pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2:Test Files 358 passed (358),Tests 5147 passed | 19 skipped (5166), VERDICT command-exit 0.pnpm --filter @objectstack/runtime typecheck: VERDICT command-exit 0,check:test-typecheck: OK. tsc -p tsconfig.test.json --listFiles lists the new test (count 1); tsconfig.json excludes tests by the package's design (count 0). (4) Regression:pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/showcase-declarative-endpoints.dogfood.test.ts:Tests 17 passed (17), VERDICT command-exit 0. (5) ABLATION via node scripts/ablation-replace.mjs (wrap mode, trap-armed): anchorthis.registerDeclaredApiEndpoints(ctx, appId);x1 -> x0, replacement x0 -> x1, blob c538f19090 -> 84eadb1af4. The subject resolves from src through relative imports (./app-plugin.js, ./standalone-stack.js), so no dist leg. Result: 3 failed / 3 passed. Red: the config-door match (expected undefined to be 'probe_task_feed'), the byte-comparable case (expected undefined to match object) and the one-conversion case (expected [] to deeply equal ['probe_task_feed']). Green: the three controls. Restore: blob c538f19090 == HEAD,git diff HEADempty. Direction as expected (turns red). (6) LINT, proven narrowing at 133543e. Population from eslint's own config: --print-config resolves 6 and 5 rules for the two touched .ts files and no config for the .md changeset. Count from --format json: files=2 errors=0 warnings=0. Invariance: eslint.config.mjs never enables type-aware linting (7 parserOptions blocks, all { ecmaVersion, sourceType }, no parserOptions.project), and its only reads are two untouched baseline JSONs at config load, so no untouched file's verdict can move. Repo-wide pnpm lint is CI's.","gates": {
"head": "133543eb42",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths: 63 commands, change set 3 paths vs merge base 9f4b652 (committed 3, working tree 0, untracked 0). The explicit-path derivation over the same 3 paths gives the identical 63. The PM's dispatch-time 67 were over the claim's paths, which included an optional dogfood path not touched here.",
"reconciliation": "dispatch-gates --ran with exit-coded lines:
Run reconciliation — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN,a DERIVED zero — all 63 recorded an exit code and none of them is 3.","commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 548 assertions over real temp git repos (real scan()/assertInputs() path)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ Protocol lockstep: PROTOCOL_VERSION major 18, @objectstack/spec@17.7.0 — the one exception, evidenced: pre mode (tag
next) and a pendingmajorfor @objectstack/spec (.changeset/11509-element-fla","node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real te",
"node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 21 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every
$TURBO_ROOT$input of a build Build Core runs) and all 11 script(s) thebuild-corejob's","node scripts/check-closing-keyword-parity.mjs :: exit 0 :: • packages/spec/CHANGELOG.md -- 8164628 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen). A new one reds here",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8863 files, 0 disagree, 0 unparseable, 138.5s (comparator self-test: 26 cases pass).",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)",
"node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference).",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see this file's header.",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 49 in the machinery's reach, 35 outside it, every exclusion explained by a declaration on its own schema.",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 35 outside the machinery)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 75 Plugin implementation(s) across 8277 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a real destroy() (0 kno",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, every excluded name sta",
"node scripts/check-registry-log-declared.mjs :: exit 0 :: payload, not read (a dir the package's own tsconfig.json excludes): packages/create-objectstack/src/templates",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 17 cases + level-vocabulary read + population declaration + real-tree selection floor.",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 31 of 279 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s).",
"node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 131 elevation read sites in 20 packages across 59 files, living in 111 symbol(s); the page cites 124 symbol(s) against 124 required, over 147 anchors and 7 file-level",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ⚠ The delta is information, not a verdict — the floors are
>=and cannot see an upward drift at all, which is why it is PRINTED. Reproduce the record: see this file's header.","node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: → the unreachable rows themselves: this command with --json",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 3041 source file(s) all carry their unit in the key name (or in a sibling
unit, or under a declared exemption: 6 declared durati","pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real te",
"pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 3072 tes",
"pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 94653 string(s) read in 1318 parsed source(s) (seen floor 40000 strings / 600 sources), no growt",
"pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate polices the census, never investmen",
"pnpm check:dts-closure :: exit 0 :: check-dts-closure: 63 built package(s) swept - 161/161 declared declaration file(s) present across 63 package(s); 0 built package(s) declare no declaration entry point and owe none.",
"pnpm check:dual-build-cjs-loads :: exit 0 (second run; the first answered exit 3 PREREQUISITE NOT MET, eight packages without dist, a non-measurement; cached build then re-run) :: check:dual-build-cjs-loads — 107 published require entry point(s) across 66 package(s) load; 717 emitted CommonJS file(s) parse",
"pnpm check:engine-double-contract :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.",
"pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (11058 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare).",
"pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, both board strategies agree, diff scope red AND gr",
"pnpm check:lean-entry-closure :: exit 0 :: Admitted set held exactly (15 packages); 6 denied names absent.",
"pnpm check:logger-receiver-detach :: exit 0 :: control corpus fired on all five detach shapes in this same run, and stayed silent on the measured
consoleand options-callback populations -- so the zero above is a reading.","pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 11049 text file(s) -- 11049 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).",
"pnpm check:objectql-double-limit :: exit 0 :: baseline key set verified against 9f4b652: no files added.",
"pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed",
"pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3356 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).",
"pnpm check:page-declaration-shape :: exit 0 :: blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:85.",
"pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth table including the inconclusive",
"pnpm check:published-files :: exit 0 :: ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a
fileswhitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config or bui","pnpm check:query-options-erasure :: exit 0 :: baseline key set verified against 9f4b652: no files added.",
"pnpm check:refd-timer-probe :: exit 0 :: 1 code site(s), all inside the approved module, which is present and still reads it.",
"pnpm check:slot-lookup :: exit 0 :: baseline key set verified against 9f4b652: no files added.",
"pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content: 60 built package(s) swept - 536 map(s), none embed source text.",
"pnpm check:stack-collection-maps :: exit 0 :: ✓ check-stack-collection-maps: 8 enumerations reconciled against 33 declared collections (17 waiver rows, each with a reason).",
"pnpm check:swallow-census-controls :: exit 0 :: Run
--self-testfor every family; seeSELF_TEST_MODESfor the whole rationale.","pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through
dist/; 53 published subpath(s) resolved through every alias table.","pnpm check:tier-file-adoption :: exit 0 :: @objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)",
"pnpm check:type-check-coverage :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.",
"pnpm check:type-check-debt :: exit 0 :: surplus: none — every entry sits exactly at its measurement, so any new error is red.",
"pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 73 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 49, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every one an array of quoted",
"pnpm check:where-matcher :: exit 0 :: baseline key set verified against 9f4b652: no files added.",
"node scripts/check-changeset-fixed.mjs :: exit 0 :: .changeset/config.json fixed group is in sync with 70 public workspace packages. (artifact roster under .changeset)",
"pnpm check:authz-resolver :: exit 0 :: check:authz-resolver: single shared authorization resolver intact; both entry points delegate. (artifact roster under packages)",
"pnpm check:error-code-casing :: exit 0 :: (artifact roster under packages)",
"pnpm check:filter-alias-parity :: exit 0 :: check:filter-alias-parity: OK (4 transport spelling(s) of the where slot, identical on both sides)",
"pnpm check:route-ledger-census :: exit 0 :: check:route-ledger-census — all 1 census sentence(s) match their arrays. (artifact roster under packages/runtime/src)"
]
},
"line_budget": "344 changed lines (+344 / -0) over 3 files vs merge base 9f4b652, under the 3000 human-merge threshold. .changeset/22989-config-boot-api-items.md +13, packages/runtime/src/app-plugin-config-door-api-items.test.ts +254 (new), packages/runtime/src/app-plugin.ts +77 (2383 -> 2460 lines). No governed surface, no skills/** file.",
"files_changed": [
".changeset/22989-config-boot-api-items.md",
"packages/runtime/src/app-plugin-config-door-api-items.test.ts",
"packages/runtime/src/app-plugin.ts"
],
"deviations": [
"PR body line 1 is
Part of #22989, not the dispatch'sFixes #22989. The role file governs a conflict: use Part of when merging should not close the card, and never Fixes a card bound for the decision box. The card's own pin (showcase os serve -> 401) is unmet. Line 2 isClause-②: no, as dispatched.","status needs_decision WITH an open draft PR. The PR is the ruled half and is required under every option in open_questions. The decision is the composition half, outside the claim surface (packages/cli or packages/core).",
"The triage pin
config boot of examples/app-showcase answers 401is not delivered as a test, because it is unattainable from the claim surface (measured 404 after the fix). It is delivered at the matcher seam on a bare-config composition, plus CLI HTTP measurements on six boots. No dogfood pin was added (optional): the dogfood harness's bootStack has no MetadataPlugin either, so a showcase pin there would be red. The existing declarative-endpoints dogfood file ran as a regression check.","Merged origin/main (9f4b652, packages/verify and cli verify only, disjoint) into the branch before gates, per AGENTS.md §10. The merge commit carries the trailer pair.",
"Scratch fixtures for the CLI measurements were created inside this worktree, untracked and never committed, and deleted before the gate run: examples/app-todo/.probe-22989, examples/app-todo/.probe-22989-min, examples/app-showcase/objectstack.probe-22989.config.ts and objectstack.probe-22989-list.config.ts.",
"A first test leg (the author's config module stays unstamped) went red on a booted kernel. The ObjectQL registry stamps those shared items in place during manifest.register, which predates this change. The leg was dropped, and the unit leg pins that this method's own copy leaves the converted item unstamped.",
"check:dual-build-cjs-loads first answered exit 3 PREREQUISITE NOT MET (no dist for 8 unrelated packages). After a cached turbo build of those 8, the second run was exit 0.",
"Zero label writes: the dispatch names no label, and skip-changeset is not owed (the diff publishes @objectstack/runtime)."
],
"mcp_calls": "0 — no MCP GitHub tool called (reads went through gh api REST GETs)",
"api_writes": "2 — both POST /repos/objectstack-ai/objectstack/dispatches (the fleet-write relay, executed as objectstack-fleet[bot]). (1) pr_create, request fw-20261012T132402Z-70fff9, run 38255426897 success: POST /repos/objectstack-ai/objectstack/pulls (draft) then POST /repos//issues/23046/assignees [os-steve], body read back 14835/14835 bytes identical. (2) this os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos//issues/22989/comments. git push of the branch is not counted as a REST write.",
"open_questions": [
{
"question": "examples/app-showcase is a HOST config, and so is every create-objectstack scaffold. On
os serve objectstack.config.tssuch a config gets no MetadataPlugin, so themetadataslot is the kernel's in-memory fallback, which has no matchEndpoint. Declaredapistherefore answer 404 on that boot even with this PR. How should the config door of a host config get the one endpoint matcher?","options": [
"A.
os servecomposes a MetadataPlugin (no artifactSource, no scanner) on a host-config boot that has none, as it already does in dev when a compiled artifact exists. The slot is then a MetadataManager on both doors, and this PR's registration fills its index. Measured: showcase plusnew MetadataPlugin({ watch: false })answers 404 at BASE and 401 with this PR. Cost: a packages/cli serve.ts composition change outside this claim. The slot occupant changes on every host-config production boot, so persistence and loader behaviour, /meta reads, the cluster bridge (attachClusterPubSub becomes available) and boot cost all move. None of that is measured yet; it needs a census plus pins. Effort M.","B. Keep the composition and make the absence loud: when a stack declares
apisand the metadata service has no matchEndpoint, the boot warns once and names the remedy. Cost S. The showcase os serve still answers 404, the card's pin stays unmet, and declared-not-enforced persists on every scaffold's source boot.","C. Give the core in-memory fallback a matchEndpoint. Listed only to refuse it: triage ruled
No second matcher, and packages/core cannot import packages/metadata, so this would be a second EndpointMatcher implementation that drifts.","D.
os serveof a host config composes the artifact door over dist/objectstack.json when that file exists (as os dev does) and declares artifact-door. Cost S-M. A source-only boot still answers 404, and a stale dist is served silently, so it does not answerfrom source."],
"recommendation": "A, with B's one-line warning kept for any composition still lacking a matcher (e.g. @objectstack/verify's lean bootStack). Real business need, measured: the only in-repo
apisproducer (the showcase) and every scaffold are host configs; the bare examples (app-crm, app-todo, app-multi-package) declare noapis. So this PR alone reaches no in-repo producer over HTTP, and A reaches all of them. Long-term soundness: one metadata service implementation (MetadataManager) and one matcher on both doors, no second matcher, no consumer-side tolerance; the registration already lives in AppPlugin. Preventing AI authoring errors: an AI author checks a declared endpoint withos servefrom source; A makes declared = enforced there; B only makes the gap loud; D can serve a stale build silently. Startup scope: A composes an existing plugin and adds no capability and no gate; B is the minimal alternative if A's census finds a cost the maintainer will not take."}
],
"out_of_scope_findings": [
"carrier: PM (the dispatch's assumption-5 measurement) · noted, not filed · On examples/app-showcase, 15 types the artifact door holds in the metadata service are absent from it on the os serve config door: app, view, page, dashboard, report, action, flow, webhook, hook, mapping, analytics_cube, connector, email_template, doc, book. Counts are in measurements.types_the_config_door_lacks. No consumer reading only the metadata service was measured, so no class is claimed. Dedupe words: config door metadata service missing types; host config metadata fallback list empty; os serve metadata.list view flow empty.",
"carrier: none · noted, not filed (zero pull) · A packages[] bundle's BODY api items still do not reach the metadata service on the config door; only the top-level residual does, through registerUnclaimedTopLevel. No in-repo stack declares apis inside packages[]. Dedupe words: multi-package config boot api body not registered; packages[] apis config door.",
"carrier: none · noted, not filed · @objectstack/verify bootStack mounts new AppPlugin(config) under the default registrar. A caller adding an artifact-door MetadataPlugin through extraPlugins (the declarative-endpoint dogfood file) now has two api registrars, as the four security collections already do. The items are equal (pinned), and the dogfood file stays 17/17. Dedupe words: bootStack extraPlugins artifact door two writers; verify harness registrar declaration.",
"carrier: none · noted, not filed · On a config boot the ObjectQL registry's manifest.register stamps _packageId / _packageVersion / _provenance IN PLACE on the api items of the author's config module: the converted definition shares them when nothing converts. Observed by a dropped test leg; no consumer harm measured. Dedupe words: manifest.register mutates config module items; registry applyProtection in place config boot."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23046 at
133543eb42(Part of #22989, the ruled half). It lands when every check on the head is green; the composition half goes to the maintainerdomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T13:31Z · holder of claim6124079823. Report:os-dev-report6125234382(needs_decision). Thread-read: 6125234382.Checklist, read against GitHub:
main, line 1Part of #22989, line 2Clause-②: no, assigneeos-steve.Fixes. The dev wrotePart of, because the card's own pin is unmet (below), and the role file rules that case. The seat agrees: the card stays open for the decision.packages/runtime/src/app-plugin.ts(+77), a new test (+254) and the changeset.origin/mainwas merged in with no conflict.check-governed-merges --pr 23046: not governed.registerDeclaredApiEndpointsregisters each declaredapiitem throughregisterInMemory, stamped byapplyProtectionwith the stack's manifest coordinates, the route the code-defined datasources take.this.forwardConverted(ctx).definition, ADR-0087 forward conversion misses two artifact doors: a ^18 artifact built on main gets no D2 replay and fails to boot; AppPlugin registers raw non-security collections, so /meta lists and the email loader read the unconverted copy #22994's one conversion, never the raw bundle.packages[]bundle alone.packages/metadatachange.EndpointMatcheris built lazily at the firstmatchEndpoint, andregisterInMemory('api', …)invalidates it. So there is no ordering constraint.os servefrom source answers401where it answered404, and its stored item equals the artifact door's.os devand artifact boots answer401before and after.404on every boot.examples/app-showcaseand everycreate-objectstackscaffold are HOST configs. Onos servethe CLI composes noMetadataPluginfor them (it composes one only on the dev path,packages/cli/src/commands/serve.tsabout:3233). So theirmetadataslot is the core in-memory fallback, which has nomatchEndpoint. Measured after this PR:count: 2registered, still404.os servecomposes aMetadataPluginon host-config boots; B, a loud warning only; C, refused (a second matcher); D, the artifact door overdist/. The dev recommends A, with B's warning for lean compositions.metadataslot's occupant on every host-config production boot. The seat puts that to the maintainer and does not rule it.app,view,page,dashboard,report,action,flow,webhook,hook,mapping,analytics_cube,connector,email_template,docandbook. Whether any consumer reads one of them only from the metadata service is NOT MEASURED. The fact belongs to the same composition question.133543eb42:@objectstack/runtime: 358 of 358 files, 5147 tests; typecheck 0.Clause-②: no. A declaredapisentry gets on the config door the answer the artifact door already gives; no accept set, export orspeckey moves.133543eb42at this stamp: latest run per name, 28 success and 6 skipped (roster), with 5 in progress. Not green yet.mcp_calls0;api_writes2 relay strokes (pr_createwith its assignee, and the report).Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Maintainer ruling: A + B.
os servecomposes aMetadataPluginon a host-config boot, and a composition that still lacks a matcher warns oncedomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T13:36Z · holder of claim6124079823. This records the maintainer's answer, given in this session's chat, to the dev'sneeds_decision(open question 1 of theos-dev-report; ACCEPT6125234382).The maintainer's choice, verbatim: 「A+B (推荐)」. The option text the maintainer chose, as the seat put it in chat:
What this rules:
A: on a HOST config (a stack whose
plugins[]holds plugin instances:examples/app-showcaseand everycreate-objectstackscaffold),os serve objectstack.config.tscomposes aMetadataPluginwith noartifactSourcewhen the composition has none. Themetadataslot is then aMetadataManageron both doors, and PR fix(runtime): the config door registers a stack's declared api items into the metadata service #23046's registration fills its endpoint index. The landing point ispackages/cli/src/commands/serve.ts, which today composes one only on the dev path (about:3233).B: a composition that still lacks a
matchEndpoint(for example@objectstack/verify's leanbootStack) warns once when the stack declaresapis, and names the remedy. ⛔ No second matcher (option C stays refused, as triage ruled).Census first: the next slice measures, before changing the composition, what moves when the slot's occupant changes on a host-config production boot:
/metaserves (the 15 types the config door lacks today,6125234382);attachClusterPubSub);It pins what it keeps.
Sequence: PR #23046 (the ruled half,
Part of #22989) lands first. The A + B slice is dispatched by this seat after it lands, because B's warning sharesapp-plugin.ts. #23029 stays serial behind both.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #23046 →
0cbfb7c32c(Part of #22989, the ruled half). The card stays open for the A + B slicedomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T14:48Z · holder of claim6124079823, released by this landing.0cbfb7c32c, a squash with one parent.origin/main'spackages/runtime/src/app-plugin.tscarriesregisterDeclaredApiEndpoints: on the config door each declaredapiitem is registered throughregisterInMemory, stamped with the stack's manifest coordinates, taken from the boot's one forward-converted copy.securityMetadataRegistrar: 'artifact-door', and leaves apackages[]bundle alone.app-plugin-config-door-api-items.test.ts.6125304337.Clause-②: no.133543eb42was success or a roster skip, andcheck-governed-merges --pr 23046reported it not governed.examples/app-showcaseand everycreate-objectstackscaffold are host configs. Theiros serveboot has noMetadataPlugin, so the registered items reach nomatchEndpointand the path still answers404.6125390666(A + B) covers it:os servecomposes aMetadataPluginon a host-config boot, and a composition that still lacks a matcher warns once. The census comes first.packages/runtime/src/app-plugin.tsand the new test, both held under claim6124079823.jobs[].timeout, tursoconfig.timeout, memoryconfig.persistence.autoSaveInterval) is silently dropped #23029 (the same file, the raw job and datasource readers) stays serial behind the A + B slice.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 4 · 2026-10-12T14:50Z
Session:
session_01VoSxBQujKLZKPwK2u5ehQ6Account:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22989-s2-host-config-metadata-doorWorktree:
objectstack-issue-22989-s2Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main0cbfb7c32c; stop on a breach and explain it in the report). This is the A + B slice the maintainer ruled in6125390666, census first:packages/cli/src/commands/serve.ts(A): on a host-config boot whose composition holds noMetadataPlugin,os servecomposes one with noartifactSource, so themetadataslot is aMetadataManageron both doors.devArtifactDoor, about:3233) is unchanged.securityMetadataRegistrarstays'app-plugin'. ⛔ It is never set to'artifact-door'for that door.packages/runtime/src/app-plugin.ts(B): insideregisterDeclaredApiEndpointsonly. When the stack declaresapisand themetadataslot has nomatchEndpoint, it warns once and names the remedy (compose aMetadataPlugin). ⛔ No second matcher. ⛔ Not the raw job and datasource readers (finding: AppPlugin hands the RAW artifact bundle to job scheduling and datasource registration, so a retired duration key (jobs[].timeout, tursoconfig.timeout, memoryconfig.persistence.autoSaveInterval) is silently dropped #23029).packages/cli/src/commands/orpackages/cli/test/, one runtime test underpackages/runtime/src/, and optionally one dogfood pin underpackages/qa/dogfood/test/..changeset/22989-*.md:@objectstack/cliand@objectstack/runtimepatch, carryingClause-②: no.packages/metadata,packages/metadata-core,packages/spec,packages/plugins/*,packages/services/*. If the census shows the composition needs an optionMetadataPlugindoes not have, stop and reportneeds_decision.Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). CeilingCONTRACT_REVIEW_TIER, reason: themetadataslot's occupant changes on every host-config production boot, so what/metaand the declared endpoints serve there moves. One contract-review-tier review is owed before the queue, judged from the card's content.check-governed-merges --testover the claimed paths: not governed.Clause-②: no
Responsibility:
packages/cli serve.ts (this lane) composes a MetadataPlugin only on the dev path, so a host-config os serve boot keeps the kernel's in-memory metadata fallback, which has no matchEndpoint | the artifact door (createStandaloneStack) composes one, and PR #23046 already registers the api items on the config door | an author serving examples/app-showcase or any create-objectstack scaffold from source gets 404 ENDPOINT_NOT_FOUND on every declared apis endpoint, measured on the PR #23046 dev's boot (count: 2 registered, still 404)Thread-read: 6126633510
Serial constraints cleared:
apisendpoints answer 404 ENDPOINT_NOT_FOUND onos servefrom source, while an artifact boot of the same stack matches them — the metadata service holds noapiitems on config boots #22989's ruled half, the same function) landed at0cbfb7c32c, as the ruling's sequence requires.packages[]body, so an option-B artifact built before a retirement is refused at boot ("packages[0]is not a package entry …dashboard.refreshIntervalwas renamed") #23027,domain:engineseat 2, draft) also editsapp-plugin.ts. Its hunk is the comment block overservicePayloadininit()(about:498, +7 −5), a region disjoint fromregisterDeclaredApiEndpoints.app-plugin.tsis not aSINGLE_CLAIM_PATHSpath, so whichever PR lands second mergesmain.packages/cli/src/commands/serve.ts(the file lists of all 16 open PRs were read in this act).jobs[].timeout, tursoconfig.timeout, memoryconfig.persistence.autoSaveInterval) is silently dropped #23029 (area:api, the same file) stays serial behind this slice.@objectstack/verifyor a platform regression test in dogfood, never an app test (epic hotcrm#1579, step 5c) #15953 slice 3 (claim6125131626), holdspackages/verifyand theos verifysurface, which is disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22989,
"status": "done",
"branch": "claude/issue-22989-s2-host-config-metadata-door",
"pr": "#23078",
"session": "session_01VoSxBQujKLZKPwK2u5ehQ6 — the dispatching PM session's id (mode:subagent), as stamped in this branch's Claude-Session trailers",
"premise_still_valid": true,
"summary": "A + B as ruled in 6125390666. A: packages/cli/src/commands/serve.ts composes new MetadataPlugin({ watch: false, config: { bootstrap: 'lazy' }, rootDir: hostRoot }) on a boot where the dev artifact door did not compose, isHostConfig(config) holds, and plugins[] holds no MetadataPlugin. It is kernel.use'd at the dev door's site with no catch, and the wrap's registrar stays 'app-plugin'. B: inside registerDeclaredApiEndpoints, a slot with no matchEndpoint and at least one declared api logs one warn per app per boot, with meta { appId, count }; registration is unchanged. The card's pin is met on the final tree 66e7f58: production os serve of examples/app-showcase answers GET /api/v1/apps/showcase/tasks with no session 401 UNAUTHENTICATED, where it answered 404 ENDPOINT_NOT_FOUND. So the PR says Fixes #22989. Draft PR #23078, assignee os-steve, 8 files, +753/-27.",
"census": {
"how_measured": "examples/app-showcase, os serve objectstack.config.ts --no-ui via packages/cli/bin/run.js, cwd the app, NODE_ENV unset, OS_CLOUD_URL=off, fresh temp sqlite per boot (OS_DATABASE_URL), OS_AUTH_SECRET set, --log-level info. BASE = a second worktree at 0cbfb7c (since removed). AFTER = branch at 88abc71, before either merge of main. Final re-probe at 66e7f58. Admin session = the first sign-up on a production DB (bootstrap platform admin), or the seeded dev admin on dev postures. Harness, scripts and logs: scratchpad only, not committed.",
"1_loader": "MetadataPlugin from the BASE tree, rootDir = showcase dir, no artifact, init+start on a capturing context. eager: _loadFromFileSystem logs 'Metadata loading complete {"totalItems":0,"registeredTypes":28}', so it loads 0 items from the showcase and nothing double-registers. lazy: logs 'lazy bootstrap — skipping filesystem priming'. CHOSEN lazy, the artifact door's shape: that door loads its artifact and never runs the priming pass, while eager with no artifact registers whatever sits under rootDir/TYPE/, unstamped, beside the wrap's copy. The pin's eager CONTROL primes object/probe_door from such a root; lazy primes none. AFTER boot: 'lazy bootstrap' x1, 'Loading metadata from file system' x0.",
"2_repository": "AFTER boot logs 'FileSystemRepository attached {"repoRoot":".../examples/app-showcase/.objectstack/metadata","watch":false}'. Attaching writes nothing: no .objectstack/metadata dir anywhere in the worktree after any boot, nor in the scripted run over an empty root (#7000 no-mkdir). rootDir MUST be hostRoot, the value createStandaloneStack hands its door as projectRoot. Without it the door roots at process.cwd(). Measured from the repo root (os serve examples/app-showcase/objectstack.config.ts): repoRoot is still the app dir.",
"3_persistence": "The sys_metadata objects were already registered at BASE through ObjectQL. The primary sqlite holds 108 tables before and after, an IDENTICAL name list including sys_metadata, sys_metadata_activation, sys_metadata_audit, sys_metadata_commit and sys_metadata_history, all 0 rows. The door's 'Registered system metadata objects' (4) adds no table and no schema-sync step. Dev postures keep sys_metadata_audit in db.telemetry.sqlite, unchanged. DatabaseLoader: no in-tree caller of setDatabaseDriver or setDataEngine (git grep over non-test .ts/.mjs outside metadata-manager.ts: 0), and 'DatabaseLoader configured' x0 on both doors. So there is no sys_metadata read-through on a cold boot; reads go to the registry plus a FilesystemLoader over hostRoot/TYPE/, the same pair the artifact door's manager holds.",
"4_meta": "GET /api/v1/meta/TYPE with an admin session, BASE vs AFTER: status, count and a whole-body hash are equal for all 22 types read. app 3, view 36, page 34, dashboard 4, report 4, action 71, flow 30, webhook 1, hook 5, mapping 1, analytics_cube 1, connector 4, email_template 25, doc 9, book 1, object 108, position 16, permission 17, capability 11, sharing_rule 2, api 2, datasource 2, all 200. NO type's answer moves. Beside the artifact door (serve --dev over dist/): equal except page and dashboard bodies, which differ between the doors equally before and after (pre-existing). registerUnclaimedTopLevel moves nothing: the showcase has no packages[] and no residual line was logged. At the final tree, after merging main, every body equals BASE except email_template, whose only differing keys are category and isSystem, retired on main by #22990. One served face moves, correctly: GET /api/v1/openapi.json listed 0 /api/v1/apps/showcase/ paths on BASE and lists 2 after (/tasks and /inquiries/purge). BASE logged 'no endpoint matcher is reachable' (/meta/api) and omitted both items; AFTER logs neither.",
"5_cluster_bridge": "Runtime composes MetadataClusterBridgePlugin on every boot (packages/runtime/src/runtime.ts). Lane 1 calls metadata.attachClusterPubSub at kernel:ready. BASE host-config boot: WARN 'metadata service does not expose attachClusterPubSub(); cross-node cache invalidation disabled' x1. AFTER: x0 (memory driver -> debug skip). Scripted bridge over a fake cluster service, BASE dist: fallback+memory warn, 0 subscriptions; fallback+redis warn, 0; MetadataManager+memory debug skip, 0; MetadataManager+redis 'bridged metadata.changed', 1 subscription, 1 publish per register(), 0 per registerInMemory(). So with a remote driver this composition attaches lane 1: the FIRST metadata.changed bridge on this boot shape, not a second broadcast. The fallback had no publisher, lane 2 (metadata.mutated, the protocol) is a different channel and owner and is unchanged, and boot-time registerInMemory publishes nothing.",
"6_boot_cost": "7 interleaved runs each, under the shared verification lock (a shared box: read the ratio). Command: node packages/cli/bin/run.js serve objectstack.config.ts --port P --no-ui, cwd examples/app-showcase, env as above, default log level. BASE vs AFTER: median time to the first HTTP answer 8472 vs 8407 ms (ranges 7538-9696 vs 7566-8691); median to the ready banner 8473 vs 8458 ms; median RSS 1.5 s after ready 460980 vs 459340 kB. No measurable cost.",
"7_dev_routes": "Production AFTER: GET /api/v1/dev/metadata-events -> 404, and the boot prints '[MetadataPlugin] dev metadata-HMR endpoints NOT mounted ... NODE_ENV unset, treated as production'. Pinned in process (the door's options, NODE_ENV=production: no route mounted; CONTROL development: GET /api/v1/dev/metadata-events mounted). Dev posture with no compiled artifact (serve --dev with no dist/, and the supervisor-named missing artifact): 404 -> 200 text/event-stream. That is the one route that moves, only under NODE_ENV=development, and every other config's dev boot already mounts it.",
"8_security_registrar": "The wrap keeps 'app-plugin'. 'Registered stack-declared security metadata {"appId":"com.example.showcase","count":23}' x1 before and x1 after, and the door registers nothing of the stack. The /meta bodies of position 16, permission 17, capability 11 and sharing_rule 2 are equal before and after. Pinned: the source pin holds the file's one 'artifact-door' spelling to the dev door's expression and asserts nothing keys on this door; the integration fixture serves its position and capability exactly once each.",
"9_order": "AFTER kernel log: 'Phase 1: Init plugins' 03.408; 'Initializing Metadata Manager' 03.553; 'MetadataPlugin providing metadata service (primary mode)' 03.556; 'Phase 2: Start plugins' 03.736; 'Registered stack-declared api endpoints in metadata registry {"count":2}' 05.274 (AppPlugin.start). So the slot holds the MetadataManager when registerDeclaredApiEndpoints runs. By construction too: init registers, start reads, and the kernel injects a core fallback only for a service no init provided.",
"10_which_boots": "REACHED: (a) non-dev os serve of a host config (measured 404->401). (b) os start beside a host config: its serve child boots non-dev and loads the host module when the artifact is the config's own compiled output, so it SHARES this code and changes too (HTTP NOT MEASURED for os start). (c) os serve --dev of a host config with no dist/ (404->401). (d) the dev path with a supervisor-named missing artifact (OS_INTERNAL_ARTIFACT_PATH naming an absent dist/objectstack.json: 404->401; its warning now reads 'Dev artifact reload not enabled: no compiled artifact at PATH'). (e) a host config under --no-server (composes; no HTTP to measure). NOT REACHED: the dev artifact door's boots (os dev; serve --dev with dist/ on disk), which stay 401 before and after; every library boot (bare config, artifact fallback), which already composes one; an OS_MODE=off or bootMode 'off' bare config, which is not a host config, keeps the fallback, and now prints B's warning (measured: 1 warning, still 404)."
},
"http_before_after": [
"os serve objectstack.config.ts, showcase host config, production: GET /api/v1/apps/showcase/tasks no session 404 ENDPOINT_NOT_FOUND -> 401 UNAUTHENTICATED; POST /api/v1/apps/showcase/inquiries/purge 404 -> 401; undeclared /api/v1/apps/showcase/no-such 404 -> 404; with session GET tasks 404 -> 200 (records)",
"the same, run from the repo root (os serve examples/app-showcase/objectstack.config.ts): AFTER 401 / undeclared 404",
"os serve --dev, no dist/ (unsupervised): 404 -> 401; undeclared 404 -> 404",
"os serve --dev, supervisor-named missing artifact: 404 -> 401; undeclared 404 -> 404",
"os serve --dev over dist/objectstack.json (the artifact door): 401 -> 401; undeclared 404 -> 404",
"os dev --fresh --no-watch: 401 -> 401; undeclared 404 -> 404",
"final tree 66e7f58, production host-config os serve: 401 / undeclared 404 / dev metadata-events 404",
"plain artifact-fallback os serve of the showcase (no config, OS_ARTIFACT_PATH) refuses at BASE: its connector providers are code in plugins[], which JSON cannot carry. So the artifact control for this stack is the dev door.",
"scratch bare stack under OS_MODE=off (one authRequired api), the B branch: 404 -> 404, warning count 0 -> 1"
],
"b_warning_text": "WARN [AppPlugin] app 'com.example.fxb' declares 1 api endpoint(s), but this boot's 'metadata' service has no endpoint matcher (matchEndpoint), so every declared path answers 404 ENDPOINT_NOT_FOUND. Compose @objectstack/metadata's MetadataPlugin on this kernel to serve them. {"appId":"com.example.fxb","count":1}",
"tests": "At 66e7f58, all through scripts/pm/os-verify-lock.sh, VERDICT command-exit 0. (1) pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2: 'Test Files 360 passed (360)', 'Tests 5156 passed | 19 skipped (5175)'. (2) pnpm --filter @objectstack/runtime typecheck: exit 0, 'check:test-typecheck: OK'; the new test is in tsconfig.test.json's program (--listFiles 1). (3) pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 'Test Files 284 passed (284)', 'Tests 4276 passed (4276)'. (4) pnpm --filter @objectstack/cli typecheck: exit 0, 'check:test-typecheck: OK'; the 3 changed src/ tests are in tsconfig.json's program and the test/ one in tsconfig.test.json's. (5) pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/serve-host-config-metadata-door.integration.test.ts test/artifact-flag-precedence.integration.test.ts: 'Test Files 2 passed (2)', 'Tests 14 passed (14)'. The rest of the cli integration tier is declared to CI. The named stay-green pins are inside these runs: serve-host-config-security-registrar.pin.test.ts and serve-host-config.test.ts (cli unit), artifact-flag-precedence (5), and app-plugin-config-door-api-items.test.ts (runtime). ABLATION A, via node scripts/ablation-replace.mjs in wrap mode (trap-armed): anchor 'await kernel.use(hostConfigMetadataDoor);' x1->x0, replacement x0->x1, blob 0fa07d1065 -> f28bcffd47. The integration file runs serve.ts from source via tsx, so no build sits in this loop. Result 1 failed | 3 passed: the 401 pin got { status: 404, code: ENDPOINT_NOT_FOUND }; the undeclared, security-once and artifact-door controls stayed green. Restore: blob == HEAD 0fa07d1065, git diff HEAD empty. An earlier built-entry run of the same ablation rebuilt the cli; ablation-dist-preflight --absent reported the marker absent from all 612 built files, and the same single leg went red. Its restore rebuilt, and the preflight found the marker in dist/commands/serve.js with the tree clean. ABLATION B: anchor "if (typeof metadata?.matchEndpoint !== 'function') {" x1->x0 (guard made false), blob afe89beb7f -> 00b2d3da1f; the subject is imported from src. Result 1 failed | 2 passed, 'expected [] to have a length of 1 but got +0'; both controls green. Restore: blob == HEAD, git diff HEAD empty. Direction as expected for both. LINT, proven narrowing at 66e7f58: --print-config resolves rules for all 7 changed .ts files (5 rules each, 6 for app-plugin.ts) and none for the .md changeset; --format json gives files=7 errors=0 warnings=0, exit 0; invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project and no projectService) and reads only two baseline JSONs this diff does not touch. Repo-wide pnpm lint is CI's.",
"gates": {
"head": "66e7f58fd5",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths: 69 commands, change set 8 paths vs merge base 0a29bec (committed 8, working tree 0, untracked 0). The explicit-path derivation over this diff's paths at e618794 printed the identical 69; the dispatch-time 68 was over the claim's paths. Added to the 69: the 5 artifact-roster gates whose roster sits under these paths (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census) and check-startup-registry-verdict. 75 run, all exit 0.",
"reconciliation": "dispatch-gates --ran with exit-coded lines: 'Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN'; 'a DERIVED zero — all 69 recorded an exit code and none of them is 3'. The final derivation printed STALE TREE: origin/main gained 2 commits after the last merge (packages/plugins/organizations, packages/verify, scripts/check-verify-stand-in-erasure.mjs). They are disjoint from this diff and not merged.",
"commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 548 assertions over real temp git repos (real scan()/assertInputs() path)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ Protocol lockstep: PROTOCOL_VERSION major 18, @objectstack/spec@17.7.0 — the one exception, evidenced: pre mode (tag
next) and a pendingmajorf…","node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot…",
"node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 21 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every
$TURBO_ROOT$input of a build Build Core…","node scripts/check-closing-keyword-parity.mjs :: exit 0 :: • packages/spec/CHANGELOG.md -- 8164628 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached…",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8885 files, 0 disagree, 0 unparseable, 178.5s (comparator self-test: 26 cases pass).",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)",
"node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference).",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see …",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 49 in the machinery's reach, 35 outside it, every exclusion explaine…",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 35 outside the machinery)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 75 Plugin implementation(s) across 8299 source(s) under packages/; every teardown-shaped method (stop / shutdown / cl…",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay gree…",
"node scripts/check-registry-log-declared.mjs :: exit 0 :: payload, not read (a dir the package's own tsconfig.json excludes): packages/create-objectstack/src/templates",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 17 cases + level-vocabulary read + population declaration + real-tree selection floor.",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 31 of 279 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s).",
"node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 131 elevation read sites in 20 packages across 59 files, living in 111 symbol(s); the page cites 124 symbol(s) again…",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ⚠ The delta is information, not a verdict — the floors are
>=and cannot see an upward drift at all, which is why it is PRINTED. Reproduce the recor…","node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: → the unreachable rows themselves: this command with --json",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 3048 source file(s) all carry their unit in the key name (or in a sibling
unit…","pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot…",
"pnpm check:cli-test-child-env :: exit 0 :: ✓ check:cli-test-child-env: 144 spawner source(s) among 288 under packages/cli/test/; no new bulk process.env copy reaches a spawned child, all 174 …",
"pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked …",
"pnpm check:dispatcher-error-vocabulary :: exit 0 :: [#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — the
apierrorargshape.APIError.from…","pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 94753 string(s) read in 1319 parsed source(s) …",
"pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consume…",
"pnpm check:dts-closure :: exit 0 :: check-dts-closure: 71 built package(s) swept - 170/170 declared declaration file(s) present across 70 package(s); 1 built package(s) declare no declar…",
"pnpm check:dual-build-cjs-loads :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see …",
"pnpm check:engine-double-contract :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.",
"pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (11092 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declar…",
"pnpm check:i18n :: exit 0 :: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).",
"pnpm check:i18n-coverage :: exit 0 :: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new).",
"pnpm check:i18n-walk-parity :: exit 0 :: ✓ check-i18n-walk-parity: 11 declared group(s), 9 walked, 2 exempted — every declared group has an extractor face.",
"pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, …",
"pnpm check:lean-entry-closure :: exit 0 :: Admitted set held exactly (15 packages); 6 denied names absent.",
"pnpm check:logger-receiver-detach :: exit 0 :: control corpus fired on all five detach shapes in this same run, and stayed silent on the measured
consoleand options-callback populations -- so th…","pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 11083 text file(s) -- 11083 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).",
"pnpm check:objectql-double-limit :: exit 0 :: baseline key set verified against 0a29bec: no files added.",
"pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed",
"pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3363 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).",
"pnpm check:page-declaration-shape :: exit 0 :: blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:85.",
"pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, …",
"pnpm check:published-files :: exit 0 :: ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a
fileswhitelist that covers every entry point plus CHANGELOG…","pnpm check:query-options-erasure :: exit 0 :: baseline key set verified against 0a29bec: no files added.",
"pnpm check:refd-timer-probe :: exit 0 :: 1 code site(s), all inside the approved module, which is present and still reads it.",
"pnpm check:route-envelope :: exit 0 :: read/write discriminator: 11 file(s) skipped as fetch readers (77 zero-argument
res.json()call(s), none swept in)","pnpm check:slot-lookup :: exit 0 :: baseline key set verified against 0a29bec: no files added.",
"pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content: 69 built package(s) swept - 552 map(s), none embed source text.",
"pnpm check:stack-collection-maps :: exit 0 :: ✓ check-stack-collection-maps: 8 enumerations reconciled against 33 declared collections (17 waiver rows, each with a reason).",
"pnpm check:swallow-census-controls :: exit 0 :: Run
--self-testfor every family; seeSELF_TEST_MODESfor the whole rationale.","pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through
dist/; 53 published subpath(s)…","pnpm check:tier-file-adoption :: exit 0 :: @objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)",
"pnpm check:type-check-coverage :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.",
"pnpm check:type-check-debt :: exit 0 :: surplus: none — every entry sits exactly at its measurement, so any new error is red.",
"pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 73 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 49, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL…",
"pnpm check:where-matcher :: exit 0 :: baseline key set verified against 0a29bec: no files added.",
"node scripts/check-changeset-fixed.mjs :: exit 0 :: ✓ .changeset/config.json "fixed" group is in sync with 70 public workspace packages.",
"pnpm check:authz-resolver :: exit 0 :: ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.",
"pnpm check:error-code-casing :: exit 0 :: half belongs to check:dispatcher-error-vocabulary, which reports its own scope.",
"pnpm check:filter-alias-parity :: exit 0 :: check:filter-alias-parity: OK (4 transport spelling(s) of the
whereslot, identical on both sides: $filter, filter, filters, where)","pnpm check:route-ledger-census :: exit 0 :: ✓ check:route-ledger-census — all 1 census sentence(s) match their arrays.",
"node scripts/check-startup-registry-verdict.mjs :: exit 0 :: ✓ startup registry verdicts: 45 startup/open-registry seam(s) across 3135 file(s), 38 read-only (legal), none recording a verdict the boot can contrad…"
]
},
"line_budget": "780 changed lines (+753 / -27) over 8 files vs merge base 0a29bec (git diff --numstat), under the 3000 human-merge threshold. packages/cli/src/commands/serve.ts +83/-19; packages/runtime/src/app-plugin.ts +19/-2; packages/cli/src/commands/serve-host-config-metadata-door.pin.test.ts +188 (new); packages/cli/test/serve-host-config-metadata-door.integration.test.ts +294 (new); packages/runtime/src/app-plugin-api-matcher-absent-warning.test.ts +128 (new); packages/cli/src/commands/serve-host-config-security-registrar.pin.test.ts +14/-6; packages/cli/src/utils/boot-preparation-parity.test.ts +4; .changeset/22989-host-config-metadata-door.md +23. No governed surface, no skills/ file.",
"files_changed": [
".changeset/22989-host-config-metadata-door.md",
"packages/cli/src/commands/serve-host-config-metadata-door.pin.test.ts",
"packages/cli/src/commands/serve-host-config-security-registrar.pin.test.ts",
"packages/cli/src/commands/serve.ts",
"packages/cli/src/utils/boot-preparation-parity.test.ts",
"packages/cli/test/serve-host-config-metadata-door.integration.test.ts",
"packages/runtime/src/app-plugin-api-matcher-absent-warning.test.ts",
"packages/runtime/src/app-plugin.ts"
],
"deviations": [
"File surface exceeded, cli test file 1: packages/cli/src/utils/boot-preparation-parity.test.ts gains one SERVE_ONLY entry (isHostConfig). serve.ts now calls isHostConfig from a boot-preparation module, and that pin requires every such call to be either a step or a declared serve-only read. Measured: without the entry the cli unit tier is '1 failed | 283 passed'. No behaviour change. The alternative that stays inside the surface, dropping isHostConfig, would widen A to the OS_MODE=off bare-config branch, which the ruling does not name.",
"File surface exceeded, cli test file 2: the existing pin serve-host-config-security-registrar.pin.test.ts is edited. Its count('new MetadataPlugin(') went 1 -> 2, now counting each construction site and the artifact source once. Its missing-artifact warning literal follows the reword below.",
"The dev door's decision and construction are unchanged, as claimed, but its missing-artifact WARNING and the comment above it are reworded. Both said the dev SSE endpoint is not mounted on that boot. That boot now composes this door, which mounts the endpoint under NODE_ENV=development (measured 404 -> 200). The warning now reads 'Dev artifact reload not enabled: no compiled artifact at PATH'.",
"The integration boots run the tsx source entry (development posture), not the built entry. check:cli-test-child-env pins the built-entry spawner population to a named set of seven files in scripts/check-cli-test-child-env.mjs, outside this claim. A first built-entry draft of the file turned that gate's self-test red. The production-only fact (no dev route) is pinned in process in the source pin, and production HTTP was measured on the showcase.",
"No dogfood pin (it was optional): the dogfood harness boots through @objectstack/verify's bootStack, which composes no MetadataPlugin, so it cannot boot this composition.",
"Merged origin/main twice, as the dispatch directs: before the PR (e618794), and again after PR #23045 landed (66e7f58). Both merges were clean. The closure was rebuilt and every test and gate re-run on the final tree. Census items 1-9 and the boot-cost runs were measured on the pre-merge tree (88abc71 vs BASE 0cbfb7c). The final tree was re-probed over HTTP (401 / 404 / dev route 404, /meta bodies, openapi).",
"Zero label writes: the dispatch names no label, and skip-changeset is not owed (the diff publishes @objectstack/cli and @objectstack/runtime).",
"Scratch fixtures and logs lived in the session scratchpad only; nothing was written into the repo beyond the 8 files. The BASE comparison worktree ../objectstack-issue-22989-s2-cmp was created and removed (node_modules deleted first; git worktree remove exit 0, no --force)."
],
"mcp_calls": "0 — no MCP GitHub tool called (reads went through gh api REST GETs)",
"api_writes": "2 — both POST /repos/objectstack-ai/objectstack/dispatches (the fleet-write relay, executed as objectstack-fleet[bot]). (1) pr_create, request fw-20261012T171347Z-26740f, run 38282823198 success: POST /repos/objectstack-ai/objectstack/pulls (draft), then POST /repos//issues/23078/assignees [os-steve]; body read back 27791/27791 bytes identical. (2) this os-dev-report comment, through scripts/pm/post-stamped.mjs: POST /repos//issues/22989/comments. git push of the branch is not counted as a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed · On a composition with no endpoint matcher, GET /api/v1/openapi.json strict-parses each stored api item, refuses it for its runtime _diagnostics key (unrecognized_keys), and logs that the item 'does not satisfy ApiEndpointSchema … fix the declaration rather than the document'. That is misleading prose: the declaration is valid. Measured on BASE's showcase os serve, for both items. This PR takes the host config off that path, and the remaining reach (OS_MODE=off, lean kernels) is NOT MEASURED, so no class is claimed. Dedupe words: openapi.json _diagnostics unrecognized_keys omitted api item; served-endpoints no matcher misleading fix the declaration.",
"carrier: none · noted, not filed (zero pull) · B returns before its check for a bundle carrying packages[], so a multi-package stack's top-level residual api items reach a matcher-less slot without the warning. No in-repo stack declares apis inside packages[]. Dedupe words: packages[] apis matcher-less warning; registerUnclaimedTopLevel api no matcher."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23078 at
66e7f58fd5(Fixes #22989, the A + B slice). It queues only after aCONTRACT_REVIEW_TIERPASS on this headdomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T17:18Z · holder of claim6126676359. Report:os-dev-report6129082934. Thread-read: 6129082934.Checklist, read against GitHub:
PR shape: draft, base
main, line 1Fixes #22989, line 2Clause-②: no, assigneeos-steve.Fixesis right: the card's pin is met on this head (below).Scope: 8 files, +753 / −27 (780 lines).
origin/mainwas merged in twice with no conflict, the second time after PR fix(metadata-core): the artifact forward conversion walks every packages[].manifest body of an option-B artifact (ADR-0130 D4) #23045 landed.check-governed-merges --pr 23078: not governed.A, read in the diff:
serve.tsconstructshostConfigMetadataDoor = new MetadataPlugin({ watch: false, config: { bootstrap: 'lazy' }, rootDir: hostRoot })under three conditions:isHostConfig(config)holds;pluginsholds noMetadataPlugin.It is
kernel.used at the dev door's site, so a boot composes at most one of the two. Thekernel.usehas no catch, so a boot that cannot compose the slot's occupant fails loudly. The wrap's registrar stays'app-plugin', and the file's one'artifact-door'spelling stays on the dev door's expression.B, read in the diff: inside
registerDeclaredApiEndpointsonly. A slot with nomatchEndpointand at least one declaredapilogs onewarnper app per boot, with{ appId, count }and the remedy. Registration is unchanged, and there is no second matcher.The census, all ten answered with a measurement (examples/app-showcase,
0cbfb7c32cagainst the branch):lazywas chosen;eagerwould primerootDir/TYPE/unstamped, beside the wrap's copy;hostRootand writes nothing at boot;sys_metadata*already registered, and noDatabaseLoaderread-through;/meta: 22 types, the same status, count and body hash before and after;openapi.jsongoes from 0 to 2 listed showcase paths, correctly;metadata.changedattaches for the first time on this boot shape; it is not a second broadcast;NODE_ENV=developmentwith no artifact does it go 404 → 200;MetadataManagerbeforeAppPlugin.startregisters;os startshares the code (its HTTP answer is NOT MEASURED; the source path is shared).The pin, measured at the door: a production host-config
os serveofexamples/app-showcaseanswersGET /api/v1/apps/showcase/taskswithout a session401where it answered404. With a session it answers200.404on every boot.os dev,serve --devoverdist/) answers401before and after.kernel.use) reds exactly the401leg; ablation B (the guard) reds exactly the warning leg. Both restores were proven against HEAD.Deviations, ruled by the seat:
boot-preparation-parity.test.tsgains oneSERVE_ONLYrow (isHostConfig); the pin reds without it. The existingserve-host-config-security-registrar.pin.test.tsfollows the second construction site and the reworded warning. Both are test-only.bootStack, which composes noMetadataPlugin, so it cannot boot this composition.Out-of-scope notes, no carrier:
openapi.json's misleading "fix the declaration" line on a matcher-less composition. This PR takes the host config off that path; the remaining reach is not measured.packages[]bundle: no in-repo stack declaresapisinsidepackages[].The seat files neither.
Changeset
.changeset/22989-host-config-metadata-door.md:@objectstack/cliand@objectstack/runtimepatch,Clause-②: no. It states each census fact an upgrading host author meets.Tests and gates at
66e7f58fd5:@objectstack/runtime: 360 files, 5156 tests.@objectstack/cliunit: 284 files, 4276 tests.artifact-flag-precedence: 14 tests.CI on
66e7f58fd5at this stamp: latest run per name, 24 success and 6 skipped (all in the roster), with 8 in progress. Not green yet.Contract review owed (the claim's ceiling): the
metadataslot's occupant changes on every host-config production boot.needs:contract-reviewgoes on PR fix(cli,runtime): a host-config os serve composes a MetadataPlugin, so its declared apis are served; a matcher-less composition warns once #23078 in this act, and an isolated review atCONTRACT_REVIEW_TIERis dispatched now. ⛔ The PR stays draft and out of the queue until a PASS on this head is on record.Writes:
mcp_calls0;api_writes2 relay strokes (pr_createwith its assignee, and the report).Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #23078 →
7f2f081e8a(Fixes #22989, the A + B slice). The card closescompleteddomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-12T18:08Z · holder of claim6126676359, released by this landing.7f2f081e8a, a squash with one parent.origin/maincarries:hostConfigMetadataDoorinpackages/cli/src/commands/serve.ts: aMetadataPluginwith no artifact,bootstrap: 'lazy'androotDir: hostRoot, on a host-config boot that no artifact door serves;packages/runtime/src/app-plugin.tsregisterDeclaredApiEndpoints.os serveofexamples/app-showcaseanswersGET /api/v1/apps/showcase/taskswithout a session401where it answered404 ENDPOINT_NOT_FOUND. An undeclared path still answers404.6129114437.CONTRACT_REVIEW_TIERrecord PASS on66e7f58fd5(6129388227on PR fix(cli,runtime): a host-config os serve composes a MetadataPlugin, so its declared apis are served; a matcher-less composition warns once #23078). The served model was checked against the reviewer's transcript.6125390666, delivered in full: PR fix(runtime): the config door registers a stack's declared api items into the metadata service #23046 (0cbfb7c32c) landed the registration; this PR lands A and B. The census's ten answers are in6129082934. On a host-config production boot:/metaanswer moved across the 22 types read;openapi.jsonnow lists the declared endpoints;metadata.changednow attaches;_diagnosticsdecoration (the #15591 shape) #23083 (finding, awaiting triage).openapi.jsonomits a valid declaredapiitem and logs "fix the declaration", because its closed re-parse refuses the read-time_diagnosticsdecoration: the The route-level seed apply cannot consume the shipping protocol's own read-back envelope:_packageId/_diagnosticssurvive the unwrap andSeedLoaderRequestSchemarejects them asunrecognized_keys#15591 shape. This landing removes the host-config reach. The reach of the other compositions is the dev's first measurement.os startbeside a host config shares this code, but its HTTP answer is not measured.service-cluster'smetadata-cluster-bridge-plugin.tsstill describe the host-config boot as the fallback shape (docs only).metadataprovider inplugins[]would now fail the boot. There is no in-tree producer.packages[]bundle. No in-repo stack declaresapisinsidepackages[].packages/cli/src/commands/serve.ts(the host-config composition), andpackages/runtime/src/app-plugin.ts(registerDeclaredApiEndpoints).jobs[].timeout, tursoconfig.timeout, memoryconfig.persistence.autoSaveInterval) is silently dropped #23029 (the same file: the raw job and datasource readers) was serial behind this card, and is now unblocked for this lane.Generated by Claude Code