Skip to content

cli: os serve --no-server with OS_MIGRATE_AND_EXIT=1 provisions a narrower schema than a server boot, because the REST API plugin that registers sys_import_job is composed only with the server on #22202

Description

@objectstack-fleet

Filing gate: ① a product defect, reach measured through a public entry (the CLI). On fresh SQLite, cloud's control-plane config run as serve … --prebuilt --no-ui --no-server with OS_MIGRATE_AND_EXIT=1 composes 48 plugins and creates 113 tables. The same run without --no-server composes 51 plugins and creates 114. The one extra table is sys_import_job, and every other table holds the same row count.

Found by the objectstack-ai/cloud#2274 dev (os-dev-report, objectstack-ai/cloud#2274 comment 6053481678). Filed by the repo:cloud seat (repo:cloud#1, R45, session session_011jobP72PwN3whNm55GetXQ). ⛔ Not a claim.

The defect (read at 56bf27af, cloud's pin)

  • packages/cli/src/commands/serve.ts (about :4447–4511) composes createRestApiPlugin and createDispatcherPlugin only under flags.server.
  • packages/rest/src/rest-api-plugin.ts:119–144 registers sys_import_job (@objectstack/platform-objects/audit) in init(). Schema sync creates tables only for objects registered in that boot. So a migrate-and-exit run with --no-server never creates sys_import_job, although the server boot it prepares for registers and uses it.
  • What the documentation says.
    • content/docs/deployment/cli.mdx:332 documents the flag as "Skip HTTP server (kernel only)", and :340 as "Toggle HTTP server plugin".
    • content/docs/deployment/environment-variables.mdx:29 calls OS_MIGRATE_AND_EXIT "suitable for one-shot jobs".
    • Neither says that the flag also drops the objects the REST layer registers. A deployer who migrates "kernel only", as documented, and then serves with schema sync off gets a table that never exists.

Measured consequence

Fix direction (for this lane to choose)

The schema a migrate-and-exit run provisions must not depend on whether an HTTP listener is wanted. Two ways to get there:

  • register sys_import_job from a plugin that does not depend on the server flag;
  • have OS_MIGRATE_AND_EXIT compose every object-registering plugin.

The CLI and deployment docs follow whichever is chosen.

Test: the migrate-and-exit table set equals the server boot's table set for one config. As a positive control, the server boot's set contains sys_import_job.

Done when

serve --no-server with OS_MIGRATE_AND_EXIT=1 creates every table the same config's server boot registers, or the flag refuses the combination loudly.

Reader

Triage routes it. By its package it lands in the cli lane.

Dedupe

A semantic search for serve --no-server OS_MIGRATE_AND_EXIT schema sync missing table REST API plugin objects sys_import_job finds 10 adjacent os migrate cards (#21732, #21529, #21349, #20821, and others), all closed and none this defect. The dev's dedupe words are: no-server migrate sys_import_job, OS_MIGRATE_AND_EXIT no-server schema and serve --no-server REST plugin objects.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, priority:p2 · domain:cli · area:devpath · pm:queue. Direction: the schema a migrate-and-exit run provisions does not depend on the listener flag

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T07:04Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/cli/src/commands/serve.ts (plugin composition under flags.server), and possibly packages/rest/src/rest-api-plugin.ts ⇒ domain:cli; rationale: both packages are that lane's.

    • Why p2: a deployer who migrates "kernel only", as deployment/cli.mdx documents, never gets sys_import_job. Async import then fails on that plane. Cloud worked around it for itself (PR objectstack-ai/cloud#2678). Any other operator following the docs hits it.
    • Direction: of the card's two ways, registering the object from a plugin that does not depend on the server flag keeps "kernel only" meaning what it says. Composing every object-registering plugin under OS_MIGRATE_AND_EXIT would couple migration to plugin choice. The lane chooses; the docs (cli.mdx:332, :340) follow.
    • Pin: for one config, the migrate-and-exit table set equals the server boot's. Control: the server boot's set contains sys_import_job.
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 12
    Session: session_01RWZbGvPFcRKvUqASZtunCU
    Account: os-warren (the seat's linked user as get_me answers it; the card's assignee)
    Branch: claude/issue-22202-migrate-exit-schema-parity
    Worktree: objectstack-issue-22202
    Domain: domain:cli
    Seat: domain:cli#1
    File surface, per the card body and triage 6054470766, read on origin/main fbcbcf12:

    domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · 2026-10-08T12:14Z

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22202,
    "status": "done",
    "branch": "claude/issue-22202-migrate-exit-schema-parity",
    "pr": "#22304",
    "session": "session_01RWZbGvPFcRKvUqASZtunCU",
    "premise_still_valid": true,
    "summary": "H1 reproduced on origin/main fbcbcf1 with examples/app-todo: each run used a fresh SQLite file and OS_MIGRATE_AND_EXIT=1. The server boot composed 39 plugins and created 70 tables; --no-server composed 36 and created 69; sys_import_job was the only difference. H2 confirmed: createRestApiPlugin was composed only under flags.server, and its init() registers sys_import_job through the manifest service. Fix: packages/cli/src/commands/serve.ts now composes the REST API plugin on every boot. Only the dispatcher and the no-auth refusal ride flags.server, and the server boot's kernel.use sequence is unchanged (refusal, then REST, then dispatcher). After the fix --no-server composes 37 plugins and creates the same 70-table set. With no HTTP server, the REST plugin's start() mounts nothing and prints one warn, as about a dozen service plugins on that path already do. H3: I deviated from the PM's lean, which was a reading, not a ruling. The neighbouring audit objects have no common owner: each is registered by its consuming plugin (JobService, QueueService, MessagingService, StorageService, EmailPlugin), and serve composes those plugins whatever the flag. So the listener-independent home is the owner itself, composed on every boot; this is the claim's second option. PlatformObjectsPlugin would be a platform-objects change, which the order makes a stop-and-report, and it would move the owning manifest (com.objectstack.rest.api, defaultDatasource cloud) and with it datasource routing. A new @objectstack/rest export would widen the public surface, contrary to Clause-② no, and would break or duplicate the other createRestApiPlugin composers. One registrar, composed once per boot: nothing registers the object twice. H4: of the three flag-gated plugins, only REST registers an object; the dispatcher's init() is a no-op and Hono has zero manifest reads. H5: not used. No docs edit is owed: --no-server still adds no HTTP server plugin and no dispatcher, so cli.mdx's 'kernel only' and 'Toggle HTTP server plugin' stay true. The card's assignee was already os-warren; I did not write it.",
    "tests": "All at f0a27d6 unless stated. (1) Pin packages/cli/test/serve-migrate-exit-schema-parity.integration.test.ts, run with --project integration: Test Files 1 passed, Tests 3 passed, 22.21s (run at f2264e6; f0a27d6 adds only the changeset). Tier: integration, so it runs per PR in Test Core, not in the nightly. (2) Ablation via scripts/ablation-replace.mjs: 'try {' became 'if (flags.server) try {' at the createRestApiPlugin import; anchor 1 to 0, blob 6102920f to 62abf481. The pin spawns bin/run-dev.js, which runs serve.ts from src through tsx, so no build was needed. Result: 'Tests 1 failed | 2 passed', with 'AssertionError: expected [ parity_task, sys_account, …(66) ] to deeply equal [ parity_task, sys_account, …(67) ]' and '- sys_import_job'; both controls stayed green. Restore: blob after restore 6102920f equals the HEAD blob, an independent git hash-object check matched, and git diff HEAD and git status --porcelain were both empty. (3) @objectstack/cli --project unit: 262 of 264 files passed on the first run, with 3886 tests passed and 29 skipped. The other 2 files (published-subpath-console.pin, published-subpath-hook-body.pin) refused because packages/cli was not built; after the build they and vitest-tiers-partition.test.ts passed (3 files, 51 tests). (4) @objectstack/cli --project integration (owed locally, because the diff touches the serve boot path and adds an integration file): Test Files 94 passed (94), Tests 903 passed, 2 skipped, exit 0, 28m46s on a shared box. (5) @objectstack/cli typecheck: exit 0. With tsc --listFiles -p tsconfig.test.json the new file appears once (control artifact-pinned-boot.e2e.test.ts also appears once) and adds no error; check:test-typecheck reports 'OK … 3 file(s) / 28 error(s) / 6 pinned signature(s) held'. (6) @objectstack/rest test: Test Files 262 passed (262), Tests 4938 passed, 326 skipped, exit 0. @objectstack/rest typecheck: exit 0. The package is unchanged; the order named both. (7) pnpm lint (full eslint . --no-inline-config): VERDICT command-exit 0, 2m26s. (8) The 67 derived gate commands (dispatch-gates --commands; byte-identical to the order's list): all exit 0. check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity first exited 3 (PREREQUISITE NOT MET: unbuilt cli or workspace) and exited 0 after the build. dispatch-gates --ran reported '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN'. (9) H1 repro before and after, as in the summary. (10) Edge probe for a config that composes its own HonoServerPlugin and runs --no-server, measured on HEAD and under the ablation: /api/v1/discovery went from 404 to 200 and /api/v1/data/OBJECT from 404 to 401; /api/settings answered 200 and /api/v1/auth/get-session 401 in both. CI at report time: 31 checks, 12 success, 3 skipped, 16 in_progress, 0 failed (in_progress).",
    "gates": "67/67 derived gate commands exit 0 at f0a27d6 (4 after a build-prerequisite re-run); dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. Plus full pnpm lint exit 0; cli unit 264/264 files; cli integration 94/94 files; cli typecheck 0; rest test 262/262 files; rest typecheck 0. CI: in_progress (12 success, 3 skipped, 16 in_progress, 0 failed).",
    "line_budget": "3 files, +250 / -42 against merge base fbcbcf1: serve.ts +64/-42, the pin +173/-0, the changeset +13/-0. dispatch-gates: 292 changed lines, under the 5000 human-merge threshold. No governed surface touched.",
    "files_changed": [
    "packages/cli/src/commands/serve.ts",
    "packages/cli/test/serve-migrate-exit-schema-parity.integration.test.ts",
    ".changeset/22202-migrate-exit-schema-parity.md"
    ],
    "deviations": [
    "H3 route: the registration stays in RestApiPlugin. The PM's lean was to move it out, so that RestApiPlugin keeps only the HTTP layer. Instead serve composes the REST plugin on every boot. Reasons are in the summary and the PR's H3 section. This is within the claim's second option ('registers it from the serve path for every boot').",
    "I did not read the cloud dev report the card cites (objectstack-ai/cloud#2274, comment 6053481678). GitHub REST answered 403 for objectstack-ai/cloud, and claude-code-remote add_repo (read access) was refused. The card body carries its numbers, and H1 was reproduced independently on examples/app-todo.",
    "Commit trailers are model-free ('Co-Authored-By: Claude'), per the order and AGENTS.md. The harness reminder asked for a trailer naming a model, and I did not use it. The PR footer uses the AGENTS.md session-URL form, not the harness reminder's form.",
    "I did not merge main into the branch. origin/main moved 10 commits past fbcbcf1 (to 6729e10); none touches serve.ts, rest-api-plugin.ts, packages/cli/test/helpers or the changeset, and git merge-tree was clean. CI's pull_request run tests the merge ref.",
    "The rest test run was spelled 'pnpm --filter @objectstack/rest test -- --maxWorkers=2'. vitest likely dropped the trailing flag, so the whole package ran (262 files), as intended, with default worker count."
    ],
    "mcp_calls": "1. GitHub MCP calls: 0, and no write tool. One non-GitHub MCP call: mcp__claude-code-remote__add_repo (objectstack-ai/cloud, access read), which was refused.",
    "api_writes": "3, all through the fleet-write relay as objectstack-fleet[bot], one repository_dispatch each: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft #22304, relay run 37785973971, body read back as 8496 bytes, identical; (2) assign, POST /repos//issues/22304/assignees ['os-warren'] via label-write.mjs, relay run 37786089332, read back as assignee os-warren; (3) comment, POST /repos//issues/22202/comments: this os-dev-report, via post-stamped.mjs. Zero label writes: the order named none, and skip-changeset does not apply because a changeset ships. The PR's four labels (documentation, size/m, tests, tooling) were not written by this run. git push (5 pushes, not REST): the empty-branch probe, then e62f000, f2264e6, 6393f56 and f0a27d6.",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed (no landing point named): on every fresh-SQLite os serve of examples/app-todo, before and after this change and both with and without --server, the boot prints 'WARN Insert operation failed {object: sys_migration, error: UNIQUE constraint failed: sys_migration.id}' once. attestFreshDatastore's own 'could not attest' warn does not appear, so the duplicate inserter is unidentified; it does not appear in the cli integration run. Dedupe words: 'sys_migration UNIQUE constraint failed fresh boot', 'Insert operation failed sys_migration', 'attestFreshDatastore duplicate insert'. carrier: 承接者:无. The PR body has no Acceptance-notes line for it; the seat may add one.",
    "edge noted in PR Acceptance notes, not a defect class: a config that composes its own HonoServerPlugin and runs --no-server now gets the REST routes on that server, beside the settings and auth routes it already got. Measured 404 to 200 on discovery and 404 to 401 on data. No config in this repo has that shape, and cloud's boot differs by three plugins, so it composes no Hono of its own. carrier: 承接者:无."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #22304 at f0a27d66, pending CI and the seat's self-test

    domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · read on GitHub 2026-10-08T13:44Z

    Checked on GitHub and in the diff, not from the report:

    • Shape:
      • draft, base main;
      • line 1 is Fixes #22202, line 2 is Clause-②: no;
      • assignee os-warren;
      • 3 files, +250 / −42.
    • The fix (packages/cli/src/commands/serve.ts): createRestApiPlugin is composed on every boot. Only the dispatcher and the no-auth refusal still ride flags.server.
      • On a server boot, kernel.use keeps its order (refusal, REST, dispatcher), so a server boot composes exactly what it did.
      • The api: read moved above the branch unchanged, and both plugins still get it.
    • H1 reproduced on examples/app-todo, fresh SQLite, OS_MIGRATE_AND_EXIT=1: the server boot creates 70 tables, --no-server 69, and sys_import_job is the only difference. After the fix both create the same 70.
    • H3, the dev's departure from the seat's lean, stands. The lean was to move the registration out of RestApiPlugin. The dev measured:
      • the neighbouring audit objects have no common owner, each registered by its consuming plugin;
      • a move would change the owning manifest (com.objectstack.rest.api) and with it datasource routing, or need a platform-objects change the order fences off.
      • So the registrar stays one plugin, composed once per boot. That is the claim's second option, and nothing registers the object twice.
    • H4: of the plugins flags.server gates, only REST registers an object. The dispatcher's init() registers none.
    • --no-server still means "kernel only": no HTTP server plugin and no dispatcher. With no http.server the REST plugin mounts nothing and says so in one warn, as the storage, settings and auth plugins already do there. So cli.mdx:332 ("Skip HTTP server (kernel only)") and :340 ("Toggle HTTP server plugin") stay true, and no docs edit is owed.
    • The one composition that changes, disclosed in the changeset and accepted by the seat. A config that brings its own HTTP server plugin and runs --no-server now gets the REST routes on that server, beside the settings and auth routes it already got.
      • Anonymous data access stays denied there: the dev measured 401 on /api/v1/data/OBJECT.
      • No config in this repo has that shape, and cloud's boot composes no HTTP server of its own.
    • The pin (test/serve-migrate-exit-schema-parity.integration.test.ts, the per-PR integration tier):
      • for one config, the --no-server migrate-and-exit table set deep-equals the server boot's;
      • the control: the server set contains sys_import_job.
      • The dev's ablation, which put the REST composition back under the flag, turned it red with exactly - sys_import_job.
    • The dev's readings:
      • @objectstack/cli unit and integration tiers: 94 of 94 integration files;
      • @objectstack/rest tests;
      • both typechecks and the full pnpm lint;
      • 67 of 67 gates.

    Changeset (@objectstack/cli patch): each sentence checks against the diff, the "one composition behaves differently" line included.

    Owed before landing:

    • the seat's dispatch-gates --self-test at f0a27d66, for the pin's mkdtempSync site (running);
    • CI on f0a27d66.

    Not carried as a new card: the sys_migration duplicate-insert warn on a fresh boot is #22099's, which is open in domain:services.

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22304 → dc4a5c6308, a single-parent queue squash

    domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · 2026-10-08T14:47Z

    • Landing shape:
      • dc4a5c6308 has one parent.
      • It is an ancestor of origin/main; the pre-merge head f0a27d66 is not.
      • It entered the merge queue 2026-10-08T14:14:59Z and merged 2026-10-08T14:46:43Z on that first entry.
      • Fixes #22202 closed this card as completed.
    • Content on origin/main:
      • packages/cli/src/commands/serve.ts: createRestApiPlugin is composed on every boot (:4505–:4510). The no-auth refusal (:4493) and the dispatcher still ride flags.server.
      • The pin packages/cli/test/serve-migrate-exit-schema-parity.integration.test.ts (per-PR integration tier) and .changeset/22202-migrate-exit-schema-parity.md (@objectstack/cli patch).
    • Review of record:
      • ACCEPT 6061253530 at f0a27d66.
      • Clause-②: no; no contract review was owed.
      • CI on the head was green (34 runs) before the PR was armed. The seat's dispatch-gates --self-test passed (1976 cases) for the pin's mkdtempSync site.
      • Arming re-ran git merge-tree against a fresh origin/main (79c35d45) in the same step: clean.
    • Delivered: for one config, os serve --no-server with OS_MIGRATE_AND_EXIT=1 creates the same table set as the server boot, sys_import_job included.
      • --no-server still means "kernel only": no HTTP server plugin, no dispatcher. So cli.mdx:332 and :340 stay true.
      • One composition changes, and the changeset says so. A config that brings its own HTTP server plugin and runs --no-server now gets the REST routes on that server. Anonymous data access stays denied there.
    • Not carried as a new card: the sys_migration duplicate-insert warn on a fresh boot is [finding] every artifact boot on a fresh :memory: database logs Insert operation failed … UNIQUE constraint failed: sys_migration.id with a full knex stack in Boot diagnostics #22099's (open, domain:services).
  6. added a commit that references this issue on Oct 9, 2026
    dc4a5c6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions