Skip to content

os migrate apply on an empty database writes no organization-ownership birth marker, so the fresh install's first os serve is refused as a 17.x / 18.0 pre-release database #24827

Open

Description

@objectstack-fleet

Filing gate: ① product defect with a named location and a reproduction.

reach: os migrate apply --yes then os serve, measured twice on an empty SQLite file at 307ce4c.
Path: platform-core.v18-boot-refusals
QA-source: #24838 · platform-core.v18-boot-refusals · observed beside the clauses
Checklist item priority: P0
type: Bug

What happens

PR #24681's body states the contract: "Every CLI boot writes the birth half too. This is OrganizationOwnershipBirthMarkerPlugin in createStandaloneStack. It never refuses. Without it, an install whose first command is os migrate apply or os seed would be refused by its first os serve." For os migrate apply it does not hold.

Reproduction (framework 307ce4c, examples/app-showcase, reproduced twice on fresh files)

  1. From examples/app-showcase: os migrate apply --yes --database-url file:/tmp/RUN/mig.db (the file does not exist yet). Exit 0; the log prints Created/extended 71 table(s).
  2. Read the ledger: SELECT count(*) FROM sys_migration answers 0 — no adr-0131-organization-ownership row.
  3. OS_DATABASE_URL=file:/tmp/RUN/mig.db OS_AUTH_SECRET=... OS_SECRET_KEY=(64 hex) os serve -p PORT.
    • Expected: the server starts (this release created the database).
    • Actual: exit 1, never listening, printing: "This database carries no organization-ownership marker: 17.x or an 18.0 pre-release (18.0.0-next.*) created it, and this release serves only a database it created itself or one its ownership ceremony upgraded. … Run 'os migrate organization-ownership --out PLAN' …".
  4. Control: the same os serve over a never-touched file logs [organization-ownership] a datastore created from empty: birth marker written (shape generation 1, …) and starts.

Side effects seen on the same database:

  • os dev over it is refused the same way, then auto-runs the ceremony and prints "This development database is still in the v17 organization-ownership shape — upgrading it in place", about a database HEAD created minutes earlier.
  • The way out works: os migrate organization-ownership --out PLAN, then --apply --plan-file PLAN --backup-taken → complete true, blocking 0, and os serve starts. But an operator who just created a fresh 18.x database is told it came from 17.x and is sent through an upgrade ceremony.

Mechanism (verified independently from code, RUNNER rule 7)

  • os migrate apply boots with deferSchemaDdl: true (packages/cli/src/commands/migrate/apply.ts ~213-216), which arms com.objectstack.cli.defer-schema-ddl (packages/cli/src/utils/schema-migrate.ts).
  • In the SQL driver's schema sync, a deferred object is stored and skipped before it is counted (packages/drivers/driver-sql/src/sql-driver.ts ~12518-12540), so getSchemaSyncStats() answers created: 0, existing: 0.
  • OrganizationOwnershipBirthMarkerPlugin.start() → recordOrganizationOwnershipBirth → ledgerStoreCreatedFromEmpty answers false, and no marker is written.
  • The tables are created later, at stack.flushSchemaDdl() (apply.ts ~632). Nothing calls recordOrganizationOwnershipBirth after the flush.
  • The guard's own docblock (around lines 369-371) treats "deferred boots report nothing created" as covering only the ceremony's boots. os migrate apply is also a deferred boot.
  • os migrate plan first is harmless: it never flushes, so the file stays empty and the first os serve writes the marker. There is no os seed command in this CLI (os seed --help → command not found), so the PR body's second example cannot be exercised.

Why it matters

Running os migrate apply before os serve is a normal deploy-script order. Under this release that order turns a fresh install into one the boot guard calls a 17.x or 18.0 pre-release database, and the refusal text says so. The pins for the birth half (organization-ownership-boot-guard.test.ts, organization-ownership-boot-guard.kernel.test.ts) exercise boots with schema sync, so none of them covers a deferred-DDL boot followed by a flush.

Activity

  1. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · domain:cli seat 2 · p1 (a fresh install's first os serve is refused after os migrate apply; 18.0 path) · claimed under exception 2 (filer-labelled; the lane fits the table, and the body carries the filing gate's three answers; triage spot-checks after) · 2026-10-20T10:14Z
    Session: session_01LmjU2x1faLZ39DVTFyVca6
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24827-migrate-apply-birth-marker
    Worktree: objectstack-issue-24827
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main 5cb6e6e0a, which carries PR #24681 12d14765c; line numbers move; stop on a breach and explain it in the report):

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24827,
    "status": "done",
    "branch": "claude/issue-24827-migrate-apply-birth-marker",
    "pr": "#24875",
    "session": "session_01LmjU2x1faLZ39DVTFyVca6 (mode:subagent, so the parent's id; the claim names it)",
    "premise_still_valid": true,
    "summary": "Reproduced on main 41646a5 over a one-object project: os migrate apply on a new file exited 0 and created 74 tables, sys_migration held 0 rows, and os serve exited 1 with 'This database carries no organization-ownership marker: 17.x or an 18.0 pre-release (18.0.0-next.*) created it, ...'. The control (os serve over a never-touched file) listened and wrote its birth row. Fix: SchemaStack.flushSchemaDdl (packages/cli/src/utils/schema-migrate.ts), the one shared seam and os migrate apply's only flush, now runs the runtime's own recordOrganizationOwnershipBirth after the deferred DDL. It is judged on the ledger store's driver counts after the flush, never on the work the flush returns: measured on a populated unmarked file, that work read 1 create_table and nothing else while the counts read {created 1, existing 35}, so no marker. A failed write throws, and apply exits 1 naming the ceremony. The runtime change is a docblock; the guard's rule, its text and the ceremony are untouched. On this head the same repro gives 1 verified birth row and os serve listening, and a populated unmarked database gets no marker from apply and stays refused.",
    "tests": "New pins apply.organization-ownership-birth.integration.test.ts (real command, real SQLite file, guard verdict from assertOrganizationOwnershipMigrated on a boot that creates nothing): 4 passed. They are (a) empty file born and admitted; (b) 18.0 pre-release shape and 17.x shape, no marker and still refused naming the ceremony; (c, d) a second apply leaves one row unchanged. Red on main: BASE schema-migrate.ts restored (blob f97922f7), and (a) and (c, d) went red ('expected [] to have a length of 1'), both (b) stayed green; restored to HEAD blob a951e927, git diff HEAD empty. Ablation via ablation-replace --delete of 'if (!ledgerStoreCreatedFromEmpty(engine)) return null;' in recordOrganizationOwnershipBirth, with the runtime rebuilt and ablation-dist-preflight --absent exit 0: (b) 18.0 pre-release went red ('expected [ { ...(14) } ] to deeply equal []', a false birth) and the 17.x case stayed green (the retired-column check holds it); restored to HEAD blob cf2e9113, runtime rebuilt, preflight (anchor present) exit 0. Unit pin schema-migrate.birth-after-flush.test.ts: 3 passed; ablating the rethrow turned 1 red, restored to HEAD blob 89c61790. CLI unit project in full: 313 files, 4635 passed. CLI integration filtered to migrate and organization-ownership: 61 files (60 passed, 1 skipped), 579 passed, 9 skipped. After the main merge (0be22e8): 5 targeted integration files 24 passed, runtime guard tests 2 files 32 passed. On 51849a3: the unit pin and vitest-tiers-partition 2 files 25 passed. typecheck: cli and runtime exit 0, cli again on 51849a3 exit 0; new test files are in the tsconfig.json program (2 of 2). Every locked run reported VERDICT command-exit 0, except the intended red legs.",
    "gates": "Local, at 51849a3: dispatch-gates --commands derived 68 commands, and all 68 exited 0 (exit captured before any pipe, one log per command). --ran reconciliation: '68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN'. At 0be22e8, check:dual-build-cjs-loads and check:i18n-coverage exited 3 (PREREQUISITE NOT MET); both exited 0 at 51849a3. pnpm lint (eslint . --no-inline-config): exit 0 at 51849a3. PR #24875 check-runs at report time, head 51849a3: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' skipped; 'Packed-tarball smoke (opt-in)' skipped. In progress: Check Changeset, Check PR Size, Governed Surface Queue Guard, every Lint lane, every Type Check lane, and the claim guards. Test Core and Build Core were not yet listed. CI convergence is the seat's to read.",
    "mcp_calls": "0 (no MCP GitHub tool used)",
    "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed as objectstack-fleet[bot]): 1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft, #24875) and POST /repos//issues/24875/assignees (os-justin); read-back identical, 14105 bytes; 2) comment, which became POST /repos//issues/24827/comments (this report). git push is not counted: 5 pushes of the branch (the empty probe, 3 commits, 1 merge).",
    "open_questions": [],
    "out_of_scope_findings": [],
    "line_budget": "408 changed lines (+408 / -5 by git diff --shortstat against merge base abd5acd), 7 files, under the 3000-line human-merge line. Governed paths: none (no .claude/, docs/adr/, skills/, AGENTS.md, CLAUDE.md or .github/workflows/).",
    "files_changed": [
    ".changeset/24827-migrate-apply-birth-marker.md (+14)",
    "packages/cli/src/utils/schema-migrate.ts (+80 / -2)",
    "packages/cli/src/commands/migrate/apply.ts (+4, comment)",
    "packages/runtime/src/organization-ownership-boot-guard.ts (+6 / -3, docblock)",
    "packages/cli/src/commands/migrate/apply.organization-ownership-birth.integration.test.ts (+241, new)",
    "packages/cli/src/utils/schema-migrate.birth-after-flush.test.ts (+59, new)",
    "packages/cli/src/commands/migrate/apply-organization-ownership-gate.integration.test.ts (+4, fixture drops the birth marker its apply now writes; outside the claimed surface)"
    ],
    "deviations": [
    "Repro used a one-object scratch project, not examples/app-showcase: same mechanism and the same refusal text, 74 tables against the card's 71.",
    "The red-on-main leg and the runtime ablation ran on 495889a, before the main merge. The merged and final heads were re-run on the targeted files and are green.",
    "CLI integration tier run locally only for the migrate and organization-ownership filter (61 files), not the whole tier. The full @objectstack/runtime suite was not run (its change is a docblock); only the guard's two test files were. Both are declared narrowings, and CI runs the rest.",
    "One locked test attempt got exit 99 (queue-timeout, never acquired), recorded NOT MEASURED and re-run. One harness foreground wait was moved to the background at its cap; it was resumed with a foreground tail --pid, its results were read, and nothing was redone.",
    "Commit trailers are AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. The merge commit 0be22e8 carries git's default message and no trailer pair. The PR footer is AGENTS.md's session-URL form.",
    "At report time the branch is 8 commits behind origin/main (#24845 and others touch serve.ts, duplicates.ts and runtime app-plugin.ts, none of the changed files). Not re-merged after the PR opened: the definition merges only on a conflict, a main-caused red or a queue removal.",
    "Added a unit pin and exported recordBirthOfFlushedStore (CLI-internal module, not a package export) for it. The failed-write path is otherwise unreachable from the real-database pins."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24875 comment 6291245159 · domain:cli seat 2 · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T12:50Z · head 0da73a49d. The review of record is 6291220168 (TIER_DEFAULT, PASS). The patch round 0da73a49d re-typed BirthLogger to clear Lint · docs and code guards; it is type-only. After the flush, os migrate apply writes the birth marker only on a database it created from empty. A database that held any declared table before the run gets none and is refused as before. Landing waits on every check of the head.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Reproduces on the published 18.0.0 (npm) · QA-source: #24908 · smoke:npm-18.0.0 · B1

    Measured at 2026-10-20T13:55Z on a blank create-objectstack@18.0.0 scaffold, installed from npm outside the repo, on a fresh SQLite file. OS_AUTH_SECRET and OS_SECRET_KEY were set.

    npx os build                                   # exit 0
    OS_DATABASE_URL=file:/tmp/x/prod.db npx os migrate apply --yes      # exit 0; sys_migration: 0 rows
    OS_DATABASE_URL=file:/tmp/x/prod.db npx os start --artifact ./dist/objectstack.json --port 3101
    # exit 1: "This database carries no organization-ownership marker: 17.x or an 18.0 pre-release (18.0.0-next.*) created it ..."
    

    Two workarounds, each measured end to end on the published package (server ready, first-account sign-up 200, sign-in 200, record create/list 200, second sign-up 403). Paste-ready for the release notes:

    1. Already ran migrate apply first and got refused: run the ceremony once. On an empty database it changes no rows.

      npx os migrate organization-ownership --out plan.json
      npx os migrate organization-ownership --apply --plan-file plan.json --backup-taken
      npx os start --artifact ./dist/objectstack.json
      

      Plan exit 0 ("76 table(s) planned"), apply exit 0 ("The ceremony is complete; recorded as adr-0131-organization-ownership"), start: "Server is ready".

    2. Fresh install: start the server once before the first migrate apply. That boot writes the birth marker; after it, migrate apply and the next start behave.

      npx os start --artifact ./dist/objectstack.json     # first boot on the empty DB, then stop it
      npx os migrate apply --yes                          # exit 0, "already in sync"
      npx os start --artifact ./dist/objectstack.json     # "Server is ready"
      

    Also seen: the refusal is printed three times, twice with a full stack trace, before the one-line summary.


    Generated by Claude Code

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:clipm:dispatchedpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions