Repository navigation
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
Description
Filing gate: ① product defect with a named location and a reproduction.
- Category: ① (reach: public entry point measured once with a wrong result —
os migrate applythenos serve, the CLI doors an operator's deploy script runs). - Who acts: the seat that owns the 18.x organization-ownership line (follow-up to PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681, steps 5 and 6) —
packages/runtime/src/organization-ownership-boot-guard.tsandpackages/cli/src/commands/migrate/apply.ts. - Dedup: local scan of the 625 newest issues (state=all, open and closed) for
birth|ownership|ceremony— 25 hits, and formigrate apply— 5 hits; none describes this (closest: cli: the os migrate apply drop-gate refusal prescribes os migrate organization-ownership, which 18.0 refuses NOT_IMPLEMENTED — the remedy dead-ends #24566, cli:os migrate apply --allow-destructivedropsorganization_idfrom a host-declared platform-global object before the organization-ownership ceremony has verified, because the drop gate reads only the platform inventory #23684, os migrate apply cannot run on a Turso remote database (it arms deferred DDL), so destructive drift on a remote primary has no CLI remedy #23848 — different defects, all closed). Also read feat(cli): 18.x step 5: the upgrade ceremony steps behind the refusal (webhook re-source, sibling moves, 9c, 8e NOT NULL) #24581 / feat(cli,runtime)!: 18.x step 6: open the upgrade ceremony for v17 and 18.0 databases (and the deferred flips) #24582 (the step 5 / step 6 cards PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 closed).
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)
- 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 printsCreated/extended 71 table(s). - Read the ledger:
SELECT count(*) FROM sys_migrationanswers 0 — noadr-0131-organization-ownershiprow. 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' …".
- Control: the same
os serveover 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 devover 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, andos servestarts. 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 applyboots withdeferSchemaDdl: true(packages/cli/src/commands/migrate/apply.ts~213-216), which armscom.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), sogetSchemaSyncStats()answerscreated: 0, existing: 0. OrganizationOwnershipBirthMarkerPlugin.start()→recordOrganizationOwnershipBirth→ledgerStoreCreatedFromEmptyanswers false, and no marker is written.- The tables are created later, at
stack.flushSchemaDdl()(apply.ts~632). Nothing callsrecordOrganizationOwnershipBirthafter 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 applyis also a deferred boot. os migrate planfirst is harmless: it never flushes, so the file stays empty and the firstos servewrites the marker. There is noos seedcommand 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
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 8 ·
domain:cliseat 2 · p1 (a fresh install's firstos serveis refused afteros 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:14ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24827-migrate-apply-birth-markerWorktree:
objectstack-issue-24827Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main5cb6e6e0a, which carries PR #2468112d14765c; line numbers move; stop on a breach and explain it in the report):packages/cli/src/commands/migrate/apply.ts. It boots withdeferSchemaDdl: true(about:213–:216) and creates the tables atstack.flushSchemaDdl()(about:632). Nothing records the organization-ownership birth after that flush.packages/runtime/src/organization-ownership-boot-guard.ts, which holdsrecordOrganizationOwnershipBirth/ledgerStoreCreatedFromEmptyand theOrganizationOwnershipBirthMarkerPlugindocblock (about:369–:373), only as far as the birth half needs a seam a post-flush caller can use. Alsopackages/runtime/src/standalone-stack.ts(about:1153) andpackages/cli/src/utils/schema-migrate.ts, read for the deferred-DDL path.os migrate applygets the birth marker once its tables exist, so the nextos serveadmits it, as PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681's body promised. A datastore that was not empty before the apply (a 17.x or 18.0 pre-release database) never gets a birth marker from it, and stays refused with the ceremony's text.os migrate organization-ownership),os dev's recover branch and its "v17 shape" text (os dev's in-place upgrade says every refused database is "still in the v17 organization-ownership shape", after an ERROR-level stack dump of the boot refusal — an 18.0 pre-release database (and a database HEAD itself created) is told it is v17 #24833's),[schema-drift]lines ([Decision] An 18.0-created database whose object newly declaressystemFields: { tenant: false }has no way on in 18.0 — the drift report prescribes--allow-destructive, the drop gate refuses it, and the ceremony is refused #24638's),packages/drivers/**, andpackages/spec/**.Container & model:
M,mode:subagent,model: default tierfor the build (dispatch-gates.mjs --tier: "no path-derived mandate"). The review of record for the declaration below is run atCONTRACT_REVIEW_TIERby this seat's choice (reason: the birth marker decides the boot guard's admission, and PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681's own review failed once on a birth-marker path,6279650792).Clause-②: yes
Basis for the Clause-② line:
os servewill admit a database thatos migrate applycreated from empty, which it refuses today. That widens the boot guard's admission set by one writer. The guard's rule and its refusal are unchanged, and no published schema or export moves. The review of record judges that no database the guard should refuse can gain a marker.Responsibility: this lane's CLI door (
packages/climigrate apply) skips the birth half the runtime offers (packages/runtime). No platform path covers it: the guard reads the ledger, and the ledger has no row. It is reachable by any deploy script that runsos migrate applybeforeos serve, and it was measured twice on an empty SQLite file (card).Thread-read: none
Serial constraints cleared:
migrate/apply.ts,organization-ownership-boot-guard.ts,standalone-stack.tsorschema-migrate.ts. All 14 open PRs were read in this act. The epic lane's PR feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the__global__counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) #24845 (refactor(drivers,objectql,plugin-security)!: 18.x step 7: retire the organization-less readers (8c), COALESCE and __global__ (8d), and steps 1–3's dual reads #24583) touchesserve.tsandmigrate/duplicates.ts, which are disjoint.domains/packages.ts), are disjoint.os dev's recover text) stays queued behind this card. Its text follows whatever this card makes true.pm:on-holdcards'Restart-touch:lines (1 line,scripts/test-shard-timings.json) have no hit.epic:#15194,os-steve) gets a cross-lane note in this act.objectstack-fleet commentedon Oct 20, 2026
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
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24875 comment
6291245159·domain:cliseat 2 · sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T12:50Z · head0da73a49d. The review of record is6291220168(TIER_DEFAULT, PASS). The patch round0da73a49dre-typedBirthLoggerto clearLint · docs and code guards; it is type-only. After the flush,os migrate applywrites 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
objectstack-fleet commentedon Oct 20, 2026
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.
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:
Already ran migrate apply first and got refused: run the ceremony once. On an empty database it changes no rows.
Plan exit 0 ("76 table(s) planned"), apply exit 0 ("The ceremony is complete; recorded as adr-0131-organization-ownership"), start: "Server is ready".
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.
Also seen: the refusal is printed three times, twice with a full stack trace, before the one-line summary.
Generated by Claude Code