Skip to content

fix(cli)!: os migrate apply exits 1 when the confirmation it needs is not given - #25004

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24949-migrate-confirmation-exit
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24949-migrate-confirmation-exit

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24949
Clause-②: no (narrowing)

os migrate apply now exits 1 when the confirmation it needs is not given (stdin is not a terminal, or --json, and no --yes), as every other os migrate subcommand with a confirmation gate already does. Nothing else about that run changes: it still applies nothing, prints the same warning and, under --json, emits the same document. An operator who answers n at the terminal prompt still gets exit 0.

Measured before the change (the CLI binary built at the branch base e4d604efd3)

The fixture is a config-only project on a SQLite file (OS_DATABASE_URL, NODE_ENV=production). It has one object whose legacy_note field was removed after a first os migrate apply --yes, so os migrate plan lists one destructive change, os24949_thing.legacy_note [drop_column]. A safe drift copy instead declares one new field (an add_column). For recorded-by and resume, a copy was seeded with one sentinel history row and one interrupted journal run, so each run reaches its gate. organization-ownership was given a plan file its own plan run wrote. "Written" compares a content hash of the schema plus every row of every table, read on a separate connection before and after each run. It is never a file hash. The terminal column used a real pseudo-terminal that answered n at the [y/N] prompt.

subcommand, writing mode, no --yes stdin /dev/null --json terminal, answered n written
apply --allow-destructive 0 0 0 nothing (the column is still there)
apply (safe drift) 0 0 n/a nothing
activity-changed-fields --apply 1 1 0 nothing
audit-mail-bodies --apply 1 1 0 nothing
audit-metadata-bodies --apply 1 1 0 nothing
comment-parent-pair --apply 1 1 0 nothing
comment-reactions --apply 1 1 0 nothing
files-to-references --apply 1 1 0 nothing
meta --stored --apply 1 1 0 nothing
multi-value-columns --apply 1 1 0 nothing
recorded-by --apply 1 1 0 nothing
resume --run RUN 1 1 0 nothing
security-catalog-overlays --apply 1 1 0 nothing
summary-nulls --apply 1 1 0 nothing
value-shapes --apply 1 1 0 nothing
organization-ownership --apply --plan-file PLAN (no --backup-taken) 1 1 1 nothing
  • Every non-interactive run printed a line naming its confirmation flag: --yes for every row except the last, which names --backup-taken. No run wrote anything without its confirmation.
  • apply was the only member answering 0. summary-nulls and value-shapes were not in the claim's candidate list. They have --yes gates too and were measured as control rows.
  • Detection is one rule across the family. Every gate is flags.json || !process.stdin.isTTY, and every confirm() returns false on a non-TTY stdin before it builds a prompt. No CI variable is read, apply included. The divergence was only the exit.
  • recorded-by --apply and resume --run gate after a plain boot. Re-measured on a copy whose config was one field ahead of the database, the content hash was unchanged: that boot added no column.

The change

  • packages/cli/src/commands/migrate/apply.ts: the text face now ends with this.exit(1) after its warning. The --json face emits its document with exit 1 (emitJson(…, 1); return;), which is the spelling apply's own three refusals already use, so the one-shot exit at the end of run() leaves with it. The warning text and the document are byte-for-byte unchanged.
  • Route. No shared helper or named exit constant exists for this. Each sibling spells it inline (printWarning(…); this.exit(1)), and CliExitCode in src/utils/format.ts is 0 | 1. A new helper adopted by the 14 sites would rewrite 13 that are already right. The pin below holds the family to one code instead, and its population is discovered.

The pin: packages/cli/test/migrate-confirmation-exit.pin.test.ts (unit tier)

  • Population, discovered. Every module under src/commands/migrate whose command class declares a yes or apply flag. That is 15 members, and the roster must equal them. A future subcommand with a confirmation gate fails the first case by id until its row is added.
  • Per member, both faces, with process.stdin.isTTY set to false by the case itself: exit 1, the text face names the member's confirmation flag, and no write seam is reached. For members that gate before the boot, the write seam is the boot. For apply it is flushSchemaDdl / applyMigrationEntries, and for recorded-by / resume the journal runner.
  • Controls. apply --allow-destructive --yes applies and exits 0. apply on a terminal answered n asks the prompt once, writes nothing and exits 0.
  • Replaced seams: the occupancy probe, the boot, the journal runner and reads, the sentinel scan, the one-shot exit and readline. The parse, the gates, emitJson and this.exit run for real. The commands are imported from src/, so no dist/ sits on the resolution path.

Evidence:

  • Red on main. apply.ts was restored from the base into the tree only (tree blob 091e6bb equals the base blob). The pin ran 2 failed, 32 passed: apply — text face and apply — --json face, each expected +0 to be 1. The restore was git checkout HEAD, and the blob 5581f5d equals HEAD, git diff HEAD is empty and git status is clean.
  • Green on the branch: 34 passed.
  • Ablations through scripts/ablation-replace.mjs. Each mutation was proven on disk (anchor 1 → 0, blob changed) and restored hash-equal to HEAD with git diff HEAD empty:
    • removing apply's text-face this.exit(1) → 1 failed (apply — text face), 33 passed;
    • apply's --json exit 1 → 0 → 1 failed (apply — --json face), 33 passed;
    • removing comment-reactions's text-face this.exit(1) → 1 failed (comment-reactions — text face), 33 passed. The per-member rows hold the siblings, not only apply.
    • A first attempt at the text-face ablation was refused by the tool before any test ran. Its replacement was a substring of the anchor, so the replacement count could not rise. The file was restored hash-equal and the ablation was re-run with a marked replacement. The first attempt was a no-op and is not counted.

After, at the public door (the CLI built from this branch)

command exit written
apply --allow-destructive, stdin /dev/null 1 nothing; the same ⚠ Confirmation required … line
apply --allow-destructive --json 1 nothing; message: confirmation_required, hint: pass --yes
apply (safe drift), stdin /dev/null 1 nothing
apply --allow-destructive, terminal answered n 0 nothing; Aborted — no changes made.
apply --allow-destructive --yes (control) 0 the column dropped; ✓ Applied 1 change(s).

Local verification (final head 5a3e45078e)

  • @objectstack/cli unit tier (vitest run --project unit): 316 files, 4684 tests passed, at ddd7dfdb62. The only later commit is a type cast in the new test file, and that file re-ran green at 5a3e45078e (34 passed).
  • Integration tier declared to CI: the diff touches no integration-tier file and no spawn entry. The one integration file whose run reaches apply's gate (plan.reads-env-files.integration.test.ts, which reads the document and not the exit) was run anyway: 6 passed.
  • pnpm --filter @objectstack/cli typecheck: exit 0 (check:test-typecheck: OK). The first run flagged one TS2352 in the new test file, fixed in 5a3e45078e.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 68 commands, and its 11 declared WIDE-population families were run too, 79 in all. 77 exited 0 on the first pass. check:dual-build-cjs-loads and check:i18n-coverage exited 3 (PREREQUISITE NOT MET: no dist/ for packages this diff does not touch). After a workspace build both exited 0. --ran printed ✓ dispatch-gates --ran: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED.
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0, ✓ This diff introduces no major bump.
  • node scripts/check-adr-0087-registration.mjs --base e4d604efd3: exit 0. It lists 24949-migrate-apply-confirmation-exit.md [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).
  • pnpm lint (the whole repository, eslint . --no-inline-config): exit 0.

Changeset: .changeset/24949-migrate-apply-confirmation-exit.md, @objectstack/cli minor, with a BREAKING banner, the FROM → TO, what is unchanged, the one-line script fix (pass --yes, or run os migrate plan to preview) and its ADR-0087 disposition. Clause-② narrowing means a CONTRACT_REVIEW_TIER review of record is owed before the queue.

Acceptance notes

  • organization-ownership's terminal decline exits 1; every other member's exits 0. Its prompt asks whether a backup was taken, and a n there is the refusal "--apply needs a backup of this database first". This was measured and left as it is: an interactive decline keeps its code, and that is a hold-off of this card. No carrier.
  • Docs that still say exit 0. content/docs/upgrading.mdx (the v18.0.0 step 8) and content/docs/releases/v18/18-0.mdx (the 18.0.0 checklist, from docs(releases): the 18.0.0 notes and upgrade checklist, walked from 17.7.0; Upgrading's table gains 18.0.0 and 17.5–17.7 #24922: "read the output, not the exit code") say a non-interactive apply without --yes still exits 0. Both describe the measured 18.0.0 walk, which stays true of 18.0.0. The release page is release-owned and is not edited in a code PR. The behaviour change reaches the next release's notes through this changeset. Carrier: the release compilation (maintainer).
  • apply's --json document names the condition in message, where its siblings use error. The document shape is a hold-off of this card, and it is unchanged. Noted only.
  • main moved 5 commits past the base while this ran, none of them under packages/cli. The branch was not merged with it.

Generated by Claude Code

…not given

A non-interactive run (non-TTY stdin, or --json) without --yes applied
nothing and exited 0; every other os migrate subcommand with a --yes gate
exits 1 there. Both faces now exit 1; an interactive decline keeps exit 0.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
Every migrate subcommand that writes behind a confirmation, discovered
from its flags, answers a non-interactive run without it with exit 1 in
both faces, names its confirmation flag, and reaches no write seam.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…ith its BREAKING banner

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 2 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/data-modeling/drivers.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/data-modeling/queries.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/deployment/cli.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/deployment/index.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/deployment/self-hosting.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/kernel/services-checklist.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/upgrading.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-3.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/releases/v17/17-5.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/releases/v17/17-6.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/releases/v17/17-7.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/releases/v18/18-0.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 30 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 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ae89e67d7e76c7b765f6b1a7d517754d906bbe3f — the merge of head 5a3e45078e45624641d5fded12beac4ff612c5dd into base 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b, 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 ae89e67d7e76c7b765f6b1a7d517754d906bbe3f && git checkout ae89e67d7e76c7b765f6b1a7d517754d906bbe3f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b 5a3e45078e45624641d5fded12beac4ff612c5dd && git checkout -B drift-repro 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b && git merge --no-ff 5a3e45078e45624641d5fded12beac4ff612c5dd

node scripts/docs-audit/affected-docs.mjs --json 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4fe3af9d7e9cb59d63b0a69217197b000fb34b2b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5a3e45078e45624641d5fded12beac4ff612c5dd
Local-runs: none
Scope: packages/cli/src/commands/migrate/apply.ts, packages/cli/test/migrate-confirmation-exit.pin.test.ts + packages/cli/src/utils/format.ts (CliExitCode, emitJson, isExitSignal), packages/cli/src/utils/one-shot-exit.ts, packages/cli/src/utils/schema-migration-plugins.ts (refuseWhenHostConfigUnloadable), every command module under packages/cli/src/commands/migrate/ (the 15 roster members and the 4 non-members account-issuer, duplicates, plan, unmapped-columns, plus index.ts), packages/cli/vitest-tiers.ts, packages/cli/tsconfig.test.json, packages/cli/package.json, packages/cli/src/commands/migrate/plan.reads-env-files.integration.test.ts, packages/cli/test/exit-signal.pin.test.ts, docs/qa/platform-checklist/areas/cli.json, skills/objectstack-upgrade/SKILL.md, content/docs/upgrading.mdx, content/docs/releases/v18/18-0.mdx, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, .github/workflows/docs-drift-check.yml. Prose face judged in ②: .changeset/24949-migrate-apply-confirmation-exit.md.

① Derived judgments

Net diff against main (merge-base e4d604efd3; origin/main at 4fe3af9d7e is 5 commits past it, none under packages/cli; the two-dot and three-dot diffs of the three paths are byte-identical): 3 files, +434 / -1. One code hunk, apply.ts:615-628.

  1. Accept-set change, right. In os migrate apply, when !flags.yes and (flags.json or !process.stdin.isTTY) and totalIntended > 0, after every refusal above the gate: the text face now runs printWarning(...) then this.exit(1) (was return, exit 0); the --json face emits the same document with emitJson(doc, 1) (was 0), which sets process.exitCode = 1 after draining the write (format.ts:114-120, 304-310) and run()'s exitOneShotCommand(process.exitCode) (apply.ts:139-153) leaves with it. refuseWhenHostConfigUnloadable between them is a no-op on this path (it returns false when the host config loaded, and the unloadable case already returned at apply.ts:401-416 above the gate). The warning line and the document (message: 'confirmation_required', hint: 'pass --yes', applied: [], skipped: drift, pending) are byte-unchanged. The thrown ExitError is rethrown by the enclosing catch (isExitSignal, apply.ts:721-724) so no second report, and finally still runs stack.shutdown().

  2. Which runs now exit 1 that exited 0 before — the complete set, four runs. (a) apply --allow-destructive, stdin not a TTY, no --yes, text face; (b) the same under --json; (c) plain apply on safe / needs_confirm drift or pending additive work, stdin not a TTY, no --yes, text face; (d) the same under --json. Nothing else can reach the hunk: it is guarded by !flags.yes, by the non-interactive test, and by totalIntended > 0.

  3. No run that should succeed is refused. Verified unchanged in the cited code: --yes skips the gate entirely (apply.ts:623); an interactive y proceeds through confirm() (apply.ts:633); an interactive n keeps printInfo('Aborted — no changes made.'); return; and leaves through the one-shot exit with code 0 (apply.ts:634) — the pin's control case asserts exactly this (one prompt, no write, exit 0); a database with nothing to apply returns at the in-sync branch (apply.ts:297-335) or the nothing_safe_to_apply branch (apply.ts:608-612), both exit 0 and both ABOVE the gate; --json --yes is untouched; the PARTIAL exit 1 after a confirmed apply is untouched. The [#13118] unloadable-host-config refusal, the issuer pre-flight, the reactions and organization-ownership refusals all sit above the gate and are unchanged.

  4. Interactive decline unchanged — yes. apply.ts:634 is not in the diff; the pin's "terminal answered n" control holds it at exit 0 with no write.

  5. "Non-interactive" detection is one rule across the family, right. Every gate in the tree reads flags.json || !process.stdin.isTTY (16 sites: apply.ts:624, activity-changed-fields.ts:120, audit-mail-bodies.ts:231, audit-metadata-bodies.ts:125, comment-parent-pair.ts:122, comment-reactions.ts:146, files-to-references.ts:204, meta.ts:1450, multi-value-columns.ts:401, recorded-by.ts:185, resume.ts:222, security-catalog-overlays.ts:205, summary-nulls.ts:167, value-shapes.ts:120; organization-ownership.ts:684-693 reads !json && confirm()), and each member's private confirm() returns false on !process.stdin.isTTY before building readline. No CI variable is read anywhere. The siblings' spelling is emitJson({ error: 'confirmation_required', hint: 'pass --yes' }, 0); this.exit(1) and printWarning(...); this.exit(1) (comment-reactions.ts:146-155 read verbatim); apply's emitJson(..., 1); return reaches the same status by its own existing refusal spelling (apply.ts:177-185 and 581-596 use it). Family exit is now one code.

  6. Public surface. CliExitCode is 0 | 1 (format.ts:54), unchanged; no helper, constant, flag, document key or export is added. Not inventing a shared helper is right: 14 sites already inline the exit and a helper would be a refactor outside the card. apply's --json document names the condition in message where the siblings use error — pre-existing, unchanged, a declared hold-off (--json documents beyond their exit).

  7. The pin's population discovers every apply-mode subcommand in the tree — yes, verified against the source. packages/cli/src/commands/migrate/ at the head holds 19 command modules plus index.ts (test files excluded by the pin's filter). Exactly 15 declare a boolean yes or apply flag — activity-changed-fields, apply, audit-mail-bodies, audit-metadata-bodies, comment-parent-pair, comment-reactions, files-to-references, meta, multi-value-columns, organization-ownership, recorded-by, resume, security-catalog-overlays, summary-nulls, value-shapes — and that is the ROSTER. The four non-members (account-issuer, duplicates, plan, unmapped-columns) and index.ts carry no isTTY test and no confirm(): they are read-only reports. Every confirmation gate in the tree sits in a module the discovery rule selects, so the roster is complete. Limit, noted and not a defect: readdirSync is non-recursive and the rule keys on the flag names yes/apply, so a future member escapes only by living in a sub-directory or by naming its confirmation flag something else — both outside the family's convention today, and the roster-equality case fails by id for every member the rule does see. The second family case (every member has a boolean json flag) is true per the tree.

  8. Per-member cases are sound. Both faces per member with process.stdin.isTTY set false by the run itself; exit read from the ExitError's oclif.exit, else the mocked one-shot exit's code, else process.exitCode; the text face must contain the member's confirmation flag (--yes, or --backup-taken for organization-ownership); the write seams untouched (the boot for pre-boot gates; flushSchemaDdl/applyMigrationEntries for apply; the journal runners for recorded-by/resume); no prompt recorded. Replaced seams are the occupancy probe, the boot, the journal reads, the sentinel scan, the one-shot exit and readline; parse, gates, emitJson and this.exit run for real. Red-on-main (2 failed: apply text and json) and the three ablations (each exactly 1 failed, including the comment-reactions sibling row) are consistent with the diff's shape — the apply rows are the only ones the hunk can flip. The tier is unit: no value import of bootSchemaStack (vi.mock only), no spawn, no driver import — vitest-tiers.ts signals (lines 160-178) agree, and tsconfig.test.json includes test/**/* so check:test-typecheck compiles it.

  9. Nothing in-tree pins the old exit. plan.reads-env-files.integration.test.ts:171 reads apply's message, not its exit; exit-signal.pin.test.ts:813/859 pin siblings at exit 1; the QA checklist cli.json:260/301 pins the --json payload, not the status; no script, workflow, Dockerfile, example or published skill runs os migrate apply non-interactively without --yes (git grep at the head: every hit is prose). skills/objectstack-upgrade/SKILL.md:192-194 already tells agents the family refuses in a non-TTY "exiting 1 with confirmation_required" — this diff brings apply to that published contract. The break is external (deploy scripts reading $?) and is carried by the changeset.

  10. The [#24949] comment matches apply.ts's existing convention (22 [#NNNN] comments at base); check-doc-authoring roots are .claude, docs, skills, content, not packages.

  11. Docs that still say "exits 0". content/docs/releases/v18/18-0.mdx:280-281 is RELEASE-OWNED (AGENTS.md Documentation Guardrails: never edited in a code PR; the changeset is the PR's input) — leaving it is right. content/docs/upgrading.mdx:383-384 (Upgrading to 18.0, step 8) is a hand-written tree page; its sentence is framed as the measured 18.0.0 walk and stays true of 18.0.0, but reads as present-tense guidance and becomes false for the cli version that ships this change. Not a defect of the diff (no rule requires a docs rider; the docs-drift job is advisory and labelled the PR documentation), but see ③ for the carrier correction.

  12. Check-runs on the head, all completed at render time: 40 names, 0 failed, 0 pending. Success: Build Core; Test Core and all six shards; Lint & Repo Gates; Lint · CI, release & engine gates; Lint · ESLint; Lint · PM dispatch-gates; Lint · docs & code guards; Lint · repo & tooling gates; Lint · scoped (query-options erasure, slot lookup & corpus, tooling self-tests); TypeScript Type Check; Type Check · consumer gates / debt ledger / source gates / workspace; Check Changeset; Governed Surface Queue Guard; Dogfood Regression Gate; Flag docs affected by code changes; Check Documentation Links; Check PR Size; Auto Label; filter; the three claim/card guards. Skipped by opt-in or path filter, not failures: 18.x upgrade harness (opt-in), Packed-tarball smoke (opt-in), Console Pin Gate, Build Docs, Dogfood shards and Verify CLI, Temporal Conformance. Nothing on this head is NOT MEASURED.

② Semver level

  • Changeset .changeset/24949-migrate-apply-confirmation-exit.md: '@objectstack/cli': minor. The diff publishes exactly one package source, packages/cli/src/commands/migrate/apply.ts (@objectstack/cli 18.0.0, not private); the test and the changeset publish nothing. One package, one bump — matches what the diff publishes.
  • Level right. (narrowing) is BREAKING (AGENTS.md Post-Task Checklist §3), and during the launch window a breaking change ships minor (scripts/check-changeset-no-major.mjs header and line 1670: "a BREAKING change; during the launch window it ships minor"). major would be refused by that guard; patch would understate a status that narrows. Check Changeset is green on the head.
  • BREAKING banner present with the bang in the title line (fix(cli)!:), and the migration carried: FROM (nothing applied, exit 0) → TO (nothing applied, exit 1), the Unchanged list (interactive n still 0, --yes applies and exits 0, nothing-to-apply exits 0 without asking — each verified against apply.ts in ①), and the one-line fix (pass --yes, or run os migrate plan). Accurate to the code.
  • Clause-②: no (narrowing) on the changeset (line 9) and the PR body (line 2). Right arm: nothing new is accepted — no key, flag, field or document shape — and a class of input the command admitted as success (a scripted run without --yes) is now refused by status. The narrowing face is what makes this CONTRACT_REVIEW_TIER review owed before the queue; the Governed Surface Queue Guard is green because no governed path is in the file list.
  • ADR-0087 disposition right. Exactly one marker: not-required (no-migration-prescription). No spec key, authorable spelling, object definition, stored shape or exported declaration moves, so objectstack migrate meta has nothing to rewrite; the remedy is a flag on the operator's command line, not a consumer-code rewrite. Mechanically consistent with the gate read at the head: the body carries no arrow rewrite, no migration-framed heading, no rewrite table and no FROM → TO label line (the **FROM:**/**TO:** bullets corroborate only when a label placeholder exists, and none does), so findMigrationPrescription returns null and the arm is not refused. The other four dispositions are closed in the marker on facts (published; no ids; no export narrowed). The "Lint · CI, release & engine gates" family is green on the head, and the dev's local run listed the changeset as [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).

③ Boundary flags

  • Dev flag — Zone 3 shared helper not taken. Answered: right. CliExitCode is 0 | 1, the 14 sibling sites inline this.exit(1) correctly, and the discovered-population pin holds the family to one code; a helper is a refactor beyond the card.
  • Deviation — origin/main not merged. Answered: verified, git diff e4d604efd3..origin/main -- packages/cli is empty, and the net diff equals the merge-base diff on all three paths. CI measured the head against current main.
  • Deviation — first battery stopped at 58/79 on TS2352, re-run on the head. Answered: every Type Check family is green on 5a3e45078e.
  • Deviation — check:dual-build-cjs-loads and check:i18n-coverage exited 3 before a workspace build, 0 after. Answered: a prerequisite, not a verdict; the Lint families carrying them are green on the head.
  • Deviation — cli unit tier ran at ddd7dfdb62. Answered: git diff --stat ddd7dfdb62..5a3e45078e is one line in the test file; Test Core (all six shards) is green on the head and is the measurement of record.
  • Deviation — commit trailer form. Not a diff matter; no gate reds on it. Noted.
  • Deviation / out-of-scope 2 — docs still saying "exits 0" not edited. Answered with a carrier correction: content/docs/releases/v18/18-0.mdx is RELEASE-OWNED and correctly left to the release compilation, which reads this changeset. content/docs/upgrading.mdx:383-384 is NOT release-owned, and the release compilation never writes it, so the dev's named carrier does not reach that page. Escalated: a docs-only follow-up on upgrading.mdx step 8 (replace "still exits 0" with the exit-1 behaviour, versioned to the cli release that carries this change) is owed to the domain:cli lane or the docs lane when that version is cut. Not blocking: the sentence stays true of the 18.0.0 walk it describes, and the published skill already states the exit-1 contract.
  • Out-of-scope 1 — organization-ownership's interactive decline exits 1. Answered: pre-existing and unchanged; its n at the backup question is the refusal "--apply needs a backup of this database first", not an "Aborted" decline, and an interactive decline is a declared hold-off of this card. Left as is; no carrier needed.
  • Out-of-scope 3 — apply's --json document says message where siblings say error. Answered: a declared hold-off, unchanged; plan.reads-env-files.integration.test.ts and the QA checklist pin message, so a shape change is its own card.
  • Hold-offs honoured: no refusal text changed; no os migrate apply and os migrate plan still name --allow-destructive as the remedy for drops that apply itself refuses (the pre-prompt listing and skip lines before the ownership ceremony, and sys_comment.reactions in the plan) #24958 surface touched (apply's pre-prompt listing and skip lines are not in the diff); no --json document shape changed; no packages/spec path.
  • open_questions: none declared, none found.
  • PR state: draft, head repo is the base repo, Fixes #24949, labels documentation / size/m / tests / tooling; the card carries the claim and this PR; the three claim/card guards are green.

Implemented-by: claude/issue-24949-migrate-confirmation-exit
Reviewed-by: session_01LmjU2x1faLZ39DVTFyVca6

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-21T00:04Z · head 5a3e45078e45624641d5fded12beac4ff612c5dd · claim 6303395862 → report 6304324751 → review of record 6304590851 (CONTRACT_REVIEW_TIER, PASS). This seat adopted it after re-reading its cited lines at source:

  • apply.ts, the confirmation gate: the non-interactive branch now emitJson(…, 1) / this.exit(1), and the interactive n keeps printInfo('Aborted — no changes made.'); return;;
  • comment-reactions.ts:146–:155: the sibling spelling, exit 1;
  • format.ts:54: CliExitCode = 0 | 1, unchanged.
Check Reading
Shape Draft, base main. First line Fixes #24949; Clause-②: no (narrowing) line-initial. Assignee os-justin = the card's. One changeset: @objectstack/cli: minor, a BREAKING banner with the script fix (pass --yes, or run os migrate plan), and ADR-0087 not-required (no-migration-prescription).
Surface 3 files, +434 / −1: one hunk in apply.ts, one new pin file, and the changeset, inside the claim. No refusal text, --json document shape, #24958 surface or packages/spec path moves. check-governed-merges.mjs --pr 25004: NOT governed.
Mechanism Of the 15 apply-mode os migrate subcommands, apply was the only one answering 0 with nothing applied (measured, both faces). Exactly four runs now exit 1: --allow-destructive and plain apply with pending work, each in the text and --json faces, non-interactive, without --yes. --yes, an interactive y / n, and nothing-to-apply keep their exits. One detection rule (flags.json || !process.stdin.isTTY) holds across the family.
Evidence The discovered-population pin (15 members, roster equality) is red on main for apply's two faces only. Three ablations each red exactly the row they touch, and each was restored hash-equal. The CLI unit tier: 316 files / 4684 tests.
Gates Check-runs on this head (41, latest per name), read at this act: 34 success, 7 skipped, 0 in_progress. Locally: 68/68 derived families and the 11 Declared WIDE population families exit 0; pnpm lint exit 0.

Accepted deviations: the review's ③.

  • No shared helper: 14 sites already inline the exit, and the pin holds the code.
  • main was not merged; it carries no packages/cli change past the base.
  • The battery was re-run at the head after a test-file type fix.

Out-of-scope dispositions (one line each):

  • content/docs/upgrading.mdx:383–:384 (18.0 step 8) says a non-interactive apply without --yes "still exits 0". That stays true of 18.0.0 but turns false for the cli release that carries this change, and the release compilation does not write that page (the review's ③) → carried by this seat to the version cut: a docs-only line naming the release that exits 1. releases/v18/18-0.mdx is release-owned and reads this changeset.
  • organization-ownership --apply's interactive n at its backup question exits 1 → Acceptance notes. It is that command's refusal, not an "Aborted" decline, and it is pre-existing.
  • apply's --json document says message where the siblings say error → Acceptance notes. It is pinned by an integration test and the QA checklist, so a shape change is its own card.

Landing: through the merge queue by this seat, now: every check on this head reads success (or an expected skip).


Generated by Claude Code

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/m tests tooling

Projects

None yet

2 participants