Skip to content

fix(cli): os dev's in-place organization-ownership upgrade names what stopped it, and the command that carries on - #26785

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-4e32d7
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-4e32d7

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26615

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:os dev 遇到旧开发库被启动保护拒绝、就地升级又没跑完时,不再只说「The upgrade did not run to a report」。它会打印仪式自己的拒绝原文(终端强制彩色时也读得到),或者仪式是怎么结束的(退出码、信号)加日志末尾;最后给出一条能直接接着跑的完整命令(带数据库和它写好的计划文件)。
  • 为什么改:卡片里开发者拉了一周 main 后启动失败,只拿到一句没有原因的话。这里在五种旧库形态上都复现不出那次具体的停止(包括 10 月 16 日的 HotCRM 真实旧库 + 已发布的 18.2.0,都升级成功),但找到了三处会把原因吞掉的代码路径,现在都会说出原因。
  • 风险与回滚:只动 os dev 的就地升级读结果和打印的那一层;生产 os migrate … --apply 仪式本身一行没改,升级前的备份照旧保留。回滚即还原 packages/cli/src/utils/dev-organization-ownership-upgrade.ts。
  • 先看:packages/cli/src/utils/dev-organization-ownership-upgrade.ts(stopCause 与 readApplyOutcome)、packages/cli/test/dev-organization-ownership-stale-column.integration.test.ts、scripts/check-cli-test-child-env.mjs(名单加一行)。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run src/utils/dev-organization-ownership-upgrade.test.ts,应见 13 条全绿,其中「a refusal printed in colour is read in the ceremony's own words」。
  • 需要你做的:无,席位落地。

What changes

os dev's in-place upgrade of a development database the organization-ownership boot guard refused (upgradeDevelopmentDatabase, ADR-0131 D10 development exception) now never ends without its cause:

  • The ceremony's refusal, in its own words, also in colour. The refusal lines are read from the ceremony log with colour stripped (stripVTControlCharacters). Before, a terminal that forces colour (FORCE_COLOR) made chalk write ESC[31m ✗ Refused: … into the log, the reader's startsWith('✗') matched nothing, and the stop printed no cause. Measured: FORCE_COLOR=1 os migrate organization-ownership --dev --apply … writes exactly that byte shape to a file. The ✗ glyph is now the printer's alone, no longer doubled (✗ ✗ Refused).
  • A stop with no refusal of its own names how it ended. The apply's exit used to be ignored. An uncaught error (printed by oclif as Error: …), a crash or a signal printed no ✗ line, so nothing was shown. Now the upgrade prints `os migrate organization-ownership --dev --apply` exited with code N (or was ended by SIGNAL, or could not be started), then the end of that run's own part of the log, without the kernel's timestamped INFO/DEBUG narration and without the plan run's output.
  • A report file that is not a report is read as none. The apply reserves its --out file before it boots, holding a reservation text. A run that stopped between the reservation and the report left that text, which the upgrade parsed as a report with no verdict and printed 「The upgrade stopped at a blocker (above)」 with nothing above. A file that did not parse threw out of os dev. Both are now "no report", with the cause. A blocked report that names no blocker prints the log's cause too.
  • No silent drop (seat ruling C, 6446501014). A stale organization_id the plan gives the column-drop fate is a column the plan accounts for, so the upgrade proceeds, but before the apply runs os dev prints one line per column-drop table whose retired column held non-null stamps: the table, how many stamped values the drop discards (the plan's fateCounts.rowsLosingTheirStamp), and that the copy keeps them. A table with no stamps is not named. A column the plan gives no fate still refuses the plan.
  • The exact command that carries on. Every failed upgrade ends on os migrate organization-ownership --dev --database-url URL --apply --plan-file PLAN --backup-taken, with the database the boot opened and the plan the upgrade wrote. The apply resumes from its checkpoint and prints the stop on the developer's terminal.

The copy taken before the upgrade is kept exactly as before. The production ceremony (os migrate organization-ownership --apply without --dev) is unchanged: no file under packages/cli/src/commands/migrate/ or packages/runtime is touched.

Tests

  • packages/cli/src/utils/dev-organization-ownership-upgrade.test.ts: six new cases (a coloured refusal, an uncaught error with kernel narration around it, a signal, the reservation text left behind, an unparseable report file, a blocked report with no blocker), and the existing cases read the un-doubled refusal and the exact command; a seventh (ruling C) pins that a sys_secret column drop discarding 3 stamps is announced exactly once before the apply, a drop with 0 stamps is not, and the upgrade completes. 13/13 (ablating the rows > 0 filter turns the C pin red: 1 failed | 12 passed).
  • packages/cli/test/dev-organization-ownership-stale-column.integration.test.ts (new, integration tier, the card's third target): a fresh os dev creates the database, the staging step gives sys_flow_credential and sys_secret the injected organization_id column and its idx_TABLE_organization_id index with unstamped rows, and deletes the marker (the 18.0 pre-release shape). A plain os dev on it is refused, upgraded in place and starts (「server started on the upgraded database」). Both columns are gone, the marker is verified, the rows are kept, and the copy still has both columns. The sys_secret row is stamped with a real sys_organization id, and os dev prints its stamped-count line before "Upgraded the development database" and does not name the unstamped sys_flow_credential. 5/5. It joins the built-entrypoint census of scripts/check-cli-test-child-env.mjs (seventeen → eighteen, with its reason).
Red first: the new cases against the base source

The base dev-organization-ownership-upgrade.ts (5cc31c7885), restored from HEAD afterwards (git diff HEAD empty), driven by a copy of the test whose fake answers the base seam's number:

× a refused plan changes nothing and boots nothing: its refusal is printed, with the copy's location
× a blocked step boots nothing: each blocker is printed, and how to resume
× ⭐ a refusal printed in colour is read in the ceremony's own words
× an apply that stopped with no refusal of its own (an uncaught error) names how it ended and the end of its log
× an apply ended by a signal says so
× a report file still holding the apply's reservation text is no report: the cause is printed, not "a blocker (above)"
× a report file that does not parse is named, never thrown
× a blocked report that names no blocker prints the log's cause instead of an empty "above"
Tests  8 failed | 4 passed (12)

The first two are the un-doubled glyph and the exact command; the other six are the swallowed causes.

Acceptance notes

  • The reported stop did not reproduce in this container. The reporter's database (.objectstack/data/backups/organization-ownership-2026-10-27T10-08-08-349Z) is on the reporter's machine. Five shapes were measured, and each completed the upgrade and started: (1) a one-object fixture on main, both columns added, no marker; (2) the same with a shape-generation-0 birth marker; (3) HotCRM afdf9a0 on main with the columns, the indexes, and stamped rows in both tables; (4) the same on the published 18.2.0 install; (5) an authentic HotCRM database written by HotCRM 4f82b81 (2026-10-16, platform 18.0.0-next.2), then started by HotCRM on the published 18.2.0. Its plan carried exactly sys_flow_credential and sys_secret as column-drop tables with the column, column-drops done {"dropped":2}. The test fixture stands in for the reporter's backup. With this change, running os dev on that backup prints the cause the ceremony hit. The ceremony.log beside the backup already holds it.
  • Target 2 measured: already handled. sys_flow_credential and sys_secret carry inventory fate column-drop (organization-ownership-inventory.ts), and the dev-mode ceremony drops both columns through the drift door, index included. The new test pins it. No ceremony change was needed.
  • The ruling's data line was decided by the seat (ruling C, 6446501014): proceed, and print what each column drop discards. A blanket refusal would refuse every 17.x dev database, because sys_audit_log and the sys_metadata family are column-drop tables and always stamped.
  • Open PRs on the file surface, read at start and before this PR: only feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 touches organization-ownership-inventory.ts (not touched here). Card cli: os migrate entry point for the plugin-audit key-material ledger-copy rewrite (the remaining half of #26640) #26687 has no PR yet.

Verification

Local subset at the head named in the report
  • pnpm --filter @objectstack/cli exec vitest run --project unit: Test Files 339 passed (339), Tests 4932 passed (4932), at 74e75e381c.
  • pnpm --filter @objectstack/cli typecheck: exit 0 (check:test-typecheck: OK).
  • vitest run --project integration test/dev-organization-ownership-stale-column.integration.test.ts: 5 passed.
  • The derived gate list (dispatch-gates.mjs --commands) was re-run at 74e75e381c after a full pnpm build and reconciled with --ran: 80 derived, 80 run, 0 NOT-MEASURED. check:dual-build-cjs-loads and check:i18n-coverage both exit 0. The full table is in the os-dev-report on the card (round 2: 6447810905).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; scoped eslint passes on the touched files.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer summary is in Chinese by the fleet's rule).

Generated by Claude Code

… stopped it

When the boot guard refuses a development database, os dev copies it and
runs the ceremony in place. An apply that wrote no report ended on
"The upgrade did not run to a report" with no cause whenever the
ceremony's refusal was coloured (FORCE_COLOR), or the apply stopped with
no refusal of its own (an uncaught error, a crash, a signal), or it left
its reservation text in the report file.

The upgrade now reads the ceremony's log with colour stripped, from where
each run started, and prints the refusal in the ceremony's own words, or
how the run ended and the end of its log. Every failed upgrade ends on
the exact command that carries on from the plan it wrote. A real os dev
test pins a database with a stale organization_id on sys_flow_credential
and sys_secret starting after the automatic upgrade.

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

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

15 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os dev (command, 41 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8b276385e6f1e2b94a4061160c148cfa56f81588 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 019ac9de6ee0ebc2d45accebc388cded5d0acd76 — the merge of head 74e75e381c7f4f1d82ad9cbd941f1951f0840561 into base 8b276385e6f1e2b94a4061160c148cfa56f81588, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 019ac9de6ee0ebc2d45accebc388cded5d0acd76 && git checkout 019ac9de6ee0ebc2d45accebc388cded5d0acd76
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8b276385e6f1e2b94a4061160c148cfa56f81588 74e75e381c7f4f1d82ad9cbd941f1951f0840561 && git checkout -B drift-repro 8b276385e6f1e2b94a4061160c148cfa56f81588 && git merge --no-ff 74e75e381c7f4f1d82ad9cbd941f1951f0840561

node scripts/docs-audit/affected-docs.mjs --json 8b276385e6f1e2b94a4061160c148cfa56f81588

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

…efore it runs

Seat ruling 6446501014 (option C): a stale organization_id the plan gives
the column-drop fate is a column the plan accounts for, so the dev
upgrade proceeds, but never silently. After the plan and before the
apply, os dev prints per table how many stamped values the drop
discards (the plan's fateCounts.rowsLosingTheirStamp) and names the copy
that keeps them. Pinned by a unit case and by the real os dev fixture,
whose sys_secret row is now stamped.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pVM4YkXVkL6VgUUhP
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-27T16:58Z · head 74e75e381c · #26615

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main, assignee os-project-manager.
    • The body carries Fixes #26615, Clause-②: no and Metadata-protocol: none, all line-initial.
    • One @objectstack/cli patch changeset.
    • check-governed-merges --pr 26785 reads NOT governed, 610 changed lines.
    • The card's newest claim (6444676930) names this PR's branch.
  2. Diff against the triage, the order and seat ruling C (6446501014); the seat read the whole non-test diff:
    • Only os dev's in-place upgrade layer moves (packages/cli/src/utils/dev-organization-ownership-upgrade.ts, one announce line in dev.ts). No file under packages/cli/src/commands/migrate/** or packages/runtime is touched, so the production ceremony is unchanged. The copy taken before the upgrade is kept.
    • It no longer swallows a cause:
      • a coloured refusal is read with colour stripped;
      • an apply that ends with no refusal of its own names its exit code or signal, plus the end of its own log;
      • a reservation-text or unparseable report file is read as no report, never as "a blocker (above)";
      • every failed upgrade ends on the exact resume command.
    • Ruling C: discardedStamps reads the reviewed plan file. Before the apply runs, it announces one line per column-drop table whose fateCounts.rowsLosingTheirStamp is above 0, with the table, the count and the copy that keeps them. A plan the upgrade cannot read names nothing here, and the apply refuses it in its own words. A column the plan gives no fate still refuses, unchanged.
  3. Tests:
    • Unit: 13/13. The C pin is red under ablation of the rows > 0 filter (1 failed | 12 passed). The round-1 red-first on base 5cc31c7885 gave 8 failed | 4 passed.
    • Integration: 5/5 on the built CLI. A stamped sys_secret row prints its count line before "Upgraded", and the unstamped sys_flow_credential is not named.
    • The cli unit tier at the head: 4932 tests.
  4. Acceptance notes:
    • The reporter's exact stop did not reproduce on five measured shapes, including an authentic 2026-10-16 HotCRM DB on the published 18.2.0. The change makes the next run print that stop.
    • Targets 2 and 3 are pinned by the integration test. Target 1 (the log on the reporter's machine) remains with the maintainer.
  5. Overlap: feat(cli): os migrate audit-key-material removes key-material columns from older ledger copies #26772 touches packages/cli/src/commands/migrate/**, none of this PR's files (the dev's read, re-checked by the seat at this stamp).
  6. Gates:
    • At this stamp every check-run on the head reads success or skipped; none reads failed or in progress.
    • Locally the dev reports 80 derived gates run after a full build, 0 NOT-MEASURED (dual-build-cjs-loads and i18n-coverage exit 0), plus the hand gates.
  7. Out of scope: none filed. Re-running os dev on the reporter's backup is the card's acceptance 1 and needs the reporter's machine.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 16:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit b198f91 Oct 27, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4e32d7 branch October 27, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants