Repository navigation
migrate plan on a Turso remote primary: a tighten_not_null (and every column entry the remote face cannot apply) is printed with the remedy os migrate apply --allow-destructive, which then names it as not applied and exits 1 #23925
Description
Filing gate ①, class (a). reach: measured at the public door by #23848's dev against a real sqld (libsql-server 0.24.32, libsql://127.0.0.1:PORT?tls=0), on PR #23923's branch (os-dev-report on #23848, out_of_scope_findings[0]): os migrate plan on a remote primary lists os23848_note.code — tighten_not_null with the remedy Run "os migrate apply --allow-destructive". That run then reports the entry in notApplied and exits 1 (PARTIAL), because the REMOTE transport runs no SQLite table rebuild.
Filed by domain:cli seat 1 (#6024) · marchtian · session session_01PFB9ZGRWYvjhTXsd8i6KBp, while reviewing PR #23923. ⛔ Not a claim. Grade and lane are triage's. Reader who acts: triage routes it (the remedy text is the shared differ's; the fix may need the driver to declare what it can express).
The producers, on origin/main
packages/drivers/driver-sql/src/schema-drift.ts:970: thetighten_not_nullentry's message endsRun "os migrate apply --allow-destructive".on every dialect.packages/cli/src/commands/migrate/plan.ts:340: the plan's footer,(add --allow-destructive for drops / tightenings).- What the remote face can apply (after PR fix(cli,driver-turso): os migrate plan/apply run on a Turso remote database and name the drift entries they cannot apply #23923):
drop_columnand the index entries (driver-turso'sapplyRemoteColumnDriftOp, which skips every other column entry). So for every other destructive column entry the printed remedy is one this transport cannot perform: a tightening, a width change, a dropped DEFAULT.
Wanted
Before the operator runs the apply, the plan says which entries this transport cannot apply, and does not offer --allow-destructive as their remedy. One way is for the driver to declare the drift ops its face can express, so the plan and the apply read one declaration. Another is a REMOTE-specific note on those entries. ⛔ Not a silent drop of the entry: it still is drift.
Pins: on a remote face (OS_TEST_SQLD_BIN-gated, as the remote-face integration tests are), a tighten_not_null is listed with a remedy that names the transport's limit, and a drop_column still carries --allow-destructive. CONTROL: a local SQLite file is unchanged.
Order: after PR #23923 (#23848) lands, since the apply's notApplied arrives with it.
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included:
turso remote tighten_not_null plan remedy os migrate apply allow-destructive cannot apply table rebuild: 19 hits, top 10 read. 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 (this producer's apply half, PR fix(cli,driver-turso): os migrate plan/apply run on a Turso remote database and name the drift entries they cannot apply #23923), driver-turso remote face: create table and add column never emit NOT NULL, so storage.notNull is unenforced and every such field reads as destructive drift #23847 (closed; remoteNOT NULLon sync), driver-turso remote mode ignores deferred DDL —os migrate planagainst a remote Turso datasource performs the DDL and the canonical backfill it was meant to preview, and prints no pending work #19823 (closed; remote deferred DDL), driver-turso / os migrate organization-ownership: the v18 ceremony cannot run on a Turso remote database (deferred DDL, drift detection and migration entries are refused on the remote face), so every hosted tenant's v18 upgrade stops #23816 (closed). None names the plan's remedy for an inexpressible entry.driver declares which migration ops it can express remote transport capability plan: 2 hits, driver-turso / os migrate organization-ownership: the v18 ceremony cannot run on a Turso remote database (deferred DDL, drift detection and migration entries are refused on the remote face), so every hosted tenant's v18 upgrade stops #23816 and driver-turso remote mode ignores deferred DDL —os migrate planagainst a remote Turso datasource performs the DDL and the canonical backfill it was meant to preview, and prints no pending work #19823, both closed and neither this.
Dedupe words: remote tighten_not_null remedy; turso remote plan inexpressible drift; migrate plan transport capability; allow-destructive remedy cannot run.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p2·domain:engine·area:devpath·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T14:01ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
domain:engine: the remedy text is the shared differ's (packages/drivers/driver-sql/src/schema-drift.ts:970), and what the remote face can apply isdriver-turso's. Both arepackages/drivers/*. The plan footer (packages/cli/src/commands/migrate/plan.ts:340) is declared on the claim, on the cross-domain exception path.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 1 · 2026-10-16T16:51Z · on triage's first grade 6217168172 (direction: the driver declares what it can express, and the plan reads that)
Session:
session_01TQLy3Yq6vF4SDpTXdr3UwiAccount:
os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23925-remote-plan-remedyWorktree:
objectstack-issue-23925Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/drivers/driver-turso/src/**: a declaration of the drift ops the remote face can apply, besideapplyRemoteColumnDriftOp/applyRemoteIndexDriftOp, read by the face that applies them;packages/drivers/driver-sql/src/schema-drift.ts: only the remedy sentence of the destructive column entries (tighten_not_nulland its siblings), and the type the declaration is spelled in;packages/cli/src/commands/migrate/plan.ts: the footer and the per-entry remedy the plan prints, read from the driver's declaration (declared todomain:cli);.changeset/23925-remote-plan-remedy.md.⛔ No
packages/spec/**. If the declaration needs a member on a published driver contract, stop and report: that is spec-lane work and a widening. ⛔ The plan does not branch on the dialect or transport name (triage's direction). Stop on any other breach and explain it in the report.Container & model:
M,mode:subagent,model: default(dispatch-gates --tierat2e1562091: no path-derived mandate)Clause-②: no
Responsibility:
os migrate plan(this repository's code) printsos migrate apply --allow-destructiveas the remedy for a drift entry the remote transport cannot apply | the apply already names those entries asnotAppliedand exits 1 (PR #23923,61e5ee9e5), so the two halves disagree | every Turso remote primary (every cloud tenant database): the operator is sent to a command that cannot perform the changeThread-read: 6217168172
Serial constraints cleared:
61e5ee9e5), which is the card's ownOrder:.schema-drift.ts,driver-turso/src/**orcli/src/commands/migrate/{plan,apply}.ts.2034975a4) and fix(driver-turso,cli): the v18 organization-ownership ceremony runs on a Turso remote database #23843, all merged.Read 2026-10-16T16:51Z.
Generated by Claude Code
test(cli): pin the plan's per-entry remedy and footer against the dri…
test(cli): read the plan pin's remedy assertions before its call count (
fix(cli,driver-turso): os migrate plan reads the driver's apply limit…
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23925,
"status": "done",
"branch": "claude/issue-23925-remote-plan-remedy",
"pr": "#24014",
"session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — subagent run, so the parent's (the domain:engine seat 2 PM) id, as the relay envelope and the commit trailers carry it",
"premise_still_valid": true,
"summary": "On a Turso REMOTE primary, os migrate plan no longer offers --allow-destructive for a drift entry that database cannot apply. driver-turso declares, once, how its REMOTE apply door answers every DriftOp type (REMOTE_DRIFT_OP_ANSWERS: applied | needs-table-rebuild | report-only, exhaustive by a satisfies clause). applyMigrationEntries now dispatches by that table, and TursoDriver.driftApplyLimit(opType) answers the plan from the same rows; it is null on the local and replica faces. The plan asks the driver that reported each entry (new SchemaStack.driftApplyLimit, the driftOwners lookup apply already routes by). An entry with a limit is still listed and counted, with its finding (the differ's --allow-destructive tail sentence removed by driver-sql's driftFindingWithoutRemedy) and the limit. The footer offers --allow-destructive only for destructive entries the database can apply, and names how many remain drift. A driver that declares nothing takes the old code path byte for byte, the entry messages keep their bytes on every face, and the --json document is unchanged. No packages/spec edit, no published-contract member, no dialect or URL branch.",
"tests": "Red at a512b91 (pins only; production code equals base 2e15620). sqld integration (libsql-server 0.24.32, archive sha256 71720fc8 checked): 1 failed / 6 passed, the new remote plan case failing with Received '...must be backfilled. Run "os migrate apply --allow-destructive".' while the local-file CONTROL passed. Unit pin plan.apply-limit.test.ts: 2 failed / 2 passed, the CONTROL and --json cases green. Green at d99ee34: sqld 7/7, unit 4/4, driver-sql schema-drift pins 3 files / 23 tests, driver-turso remote drift pins 4 files / 118 tests. Ablation at d99ee34: REMOTE_DRIFT_OP_ANSWERS tighten_not_null 'needs-table-rebuild' changed to 'applied' through scripts/ablation-replace.mjs WRAP with a trap restore; on disk the anchor went 1 to 0 and the blob 1a3a5461 to 37038760; turso-remote-drift-apply-limit.test.ts went red 1 failed / 3 passed (expected null to be 'it needs the SQLite table rebuild...'); restored to HEAD blob 1a3a5461 with git diff HEAD empty; no build needed (relative src import). Suites at d99ee34: driver-turso 92 files, 2429 passed / 33 skipped; driver-sql 223 passed + 11 skipped files, 3677 passed / 208 skipped tests. After merging origin/main (59f489f): typecheck exit 0 for cli (tsc + check:test-typecheck OK), driver-turso and driver-sql; new test files in each tsc program (--listFiles 1 hit each); cli unit tier 3 shards 103+102+102 files, 1754+1335+1482 tests all passed; sqld apply.remote-face 7/7 plus organization-ownership.remote-face 1/1; targeted pins cli 3 files / 15, turso 3 / 28, sql 1 / 5. cli integration, run locally: artifact-boot-migration.unbuildable-index 3/3, plan.telemetry-sibling + plan.deferred-reads + plan.reads-env-files 3 files / 10 tests; the rest of the tier is declared to CI. End to end through bin/run-dev.js on a sqld this run started and killed by PID: migrate plan exit 0 prints the tightening and the relax with '"os migrate apply" cannot apply it on this database: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run.', the drop with its --allow-destructive remedy, and the footer 'Apply with: os migrate apply (add --allow-destructive for the 1 destructive change(s) it can apply on this database)'.",
"gates": "Local, at 59f489f (HEAD = the merge of origin/main). dispatch-gates --commands --repo objectstack-ai/objectstack gave 69 commands, each run with its exit captured before any pipe. --ran reconciliation with ':: exit N' lines: 69 derived, 67 run exit 0, 2 NOT-MEASURED (both exit 3 PREREQUISITE NOT MET: check:dual-build-cjs-loads, packages outside this build closure have no dist; check:i18n-coverage, a partial round), 0 UNRUN. The PM's 63 commands at 2e15620 are all among the 69; the 6 extra (engine-double-contract, objectql-double-limit, query-options-erasure, type-check-coverage, type-check-debt, where-matcher) come from schema-migrate.ts and the driver-sql entry. The full derivation was read: 51 artifact-roster families, of which 4 keep their roster in a directory this diff touches (check-changeset-fixed under .changeset ran exit 0; authz-resolver, error-code-casing and filter-alias-parity are irrelevant: no new route, error code or filter alias). 11 declared WIDE-population families are CI's. Also run: check:nul-bytes exit 0, and ESLint --no-inline-config over the 9 changed TS files exit 0 with no output. CI: PR #24014's check-runs at 59f489f, read 18:43Z, 37 runs, none red. Success (14): Check Changeset, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Check PR Size, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, The card this PR closes must claim this branch, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Auto Label, filter. Skipped by their selectors (6): Build Docs, Console Pin Gate, Dogfood Regression Gate (shards), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). In progress (17): Build Core, Test Core 1/6 to 6/6, Type Check · workspace / source gates / consumer gates / debt ledger, Lint · ESLint, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus. Not waited on, per the contract: in_progress is the honest value.",
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "3 REST writes, in 2 fleet-relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]). Stroke 1 (run 38949114224) was pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft, #24014) then POST /repos//issues/24014/assignees (os-tesla); the relay read-back reported 14344 bytes sent and 14344 stored, assignee listed. Stroke 2 is this os-dev-report: POST /repos//issues/23925/comments. git push is not counted. No label write.",
"open_questions": [
{
"question": "Should this card be Clause-② yes (widening) rather than the claim's no? The diff adds two additive public members: TursoDriver.driftApplyLimit (on @objectstack/driver-turso's exported class) and driftFindingWithoutRemedy (a new value export of the @objectstack/driver-sql entry). The line's question (does the card widen an accept set or the public surface?) reads yes on a public surface. The PR and changeset carry the claim's 'Clause-②: no' with patch bumps, as dispatched.",
"options": [
"A: keep no and patch. Both members exist only to serve os migrate plan. No spec, contract or accept set moves, and the semver line stays as dispatched. Cost: the release notes read as a fix while the packages gained API.",
"B: yes (widening), with minor bumps for driver-sql and driver-turso. The seat rewrites the PR line and the changeset (dev does not PATCH the body). It owes the TIER_DEFAULT review of record that dispatch-gates names for a 'Clause-②: yes' declaration. Cost: one changeset edit and one review."
],
"recommendation": "B. Real need: each member has exactly one measured consumer (the plan), so the surface is real, not speculative. Long-term soundness: an exported method other drivers may now be asked to implement is a minor in semver terms, and calling it a patch mislabels the contract. Preventing AI mistakes: a declared widening puts driftApplyLimit in the changelog an upgrading agent greps, while a 'no' hides the seam a new driver face should declare. Startup focus: the surface is already the minimum (one method, one function, no spec member), so B adds no scope, only an honest label. If the seat reads 'public surface' as the spec and contract layer only, A stands."
}
],
"out_of_scope_findings": [
"class: a · reach: public door, measured. 'os migrate apply --allow-destructive --yes' (human face) through bin/run-dev.js on a real sqld at d99ee34: its listing before the prompt ends the tighten_not_null entry with 'Run "os migrate apply --allow-destructive".', then names that entry not applied and exits 1 PARTIAL. Without --allow-destructive, its line 'N destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them)' counts a tightening a re-run cannot apply either. Evidence: transcript excerpt in PR #24014, Acceptance note 1. The producer is packages/cli/src/commands/migrate/apply.ts (renderPlan(drift) without a describe, and the deferred-count lines). The seam to fix it now exists: stack.driftApplyLimit(entry), renderPlan(drift, describe), driftFindingWithoutRemedy. Same family as this card (the plan half), so fold it into that family's closing card, not a single-point card · dedupe words: migrate apply listing remote remedy; allow-destructive re-run skipped remote tightening; apply pre-confirmation turso remote",
"carrier: none · noted, not filed. The --json plan document's changes[] carries no per-entry mark of an entry its database cannot apply (the message keeps the shared remedy). Adding one widens the document. No consumer of the plan's JSON was measured to need it, so it is left as is (PR Acceptance note 2)."
],
"deviations": [
"File surface additions, declared in the PR body. (1) packages/cli/src/utils/schema-migrate.ts: the optional SqlDriverLike.driftApplyLimit member, SchemaStack.driftApplyLimit(entry) and an optional describe argument to renderPlan. This is the seam os migrate apply already reaches drivers through (dispatch Section 2.2, 'use the same seam'); apply passes no describe, so its output is unchanged. (2) packages/drivers/driver-sql/src/index.ts: one re-export line, so the plan can lazily import driftFindingWithoutRemedy.",
"The suggested route, appliableDriftOps(): ReadonlySet, was replaced by a per-op answer table plus driftApplyLimit(opType): string | null. A plain set cannot tell 'not this transport's limit' from 'no face applies it' (unbuildable_index and the manual_* findings), and would print the rebuild reason against them.",
"The plan's sqld pins were added to the existing apply.remote-face.integration.test.ts (beside plan.ts, the same fixture and the existing remote plan case) rather than to a new file.",
"The merge commit of origin/main was amended before its first push, to carry the model-free trailer pair.",
"Pre-merge cli unit shard 1 had one prerequisite failure (test/published-subpath-console.pin.test.ts: packages/cli not built). After building cli it passed 14/14 alone, and all post-merge shards passed.",
"The driver-sql and driver-turso full suites ran at d99ee34, before the merge. The merge brought nothing into either package; the post-merge reruns are the targeted pins and the typechecks.",
"Stray file: a mis-scoped redirect (a variable set only inside a backgrounded && chain) wrote a 6-byte /build1.pid at the filesystem root. The harness safety check blocked its removal, so it is left for a human to delete. Nothing in the repo is affected.",
"Scratch kept outside the repo: /tmp/claude-0/-home-user/c5f79d24-82a0-5900-a5c3-c8ad369dbef3/scratchpad/issue-23925 (logs, the e2e fixture, the PR body, the ablation script)."
],
"files_changed": [
".changeset/23925-remote-plan-remedy.md +13/-0",
"packages/cli/src/commands/migrate/apply.remote-face.integration.test.ts +65/-0",
"packages/cli/src/commands/migrate/plan.apply-limit.test.ts +235/-0 (new)",
"packages/cli/src/commands/migrate/plan.ts +52/-3",
"packages/cli/src/utils/schema-migrate.ts +39/-2",
"packages/drivers/driver-sql/src/index.ts +3/-0",
"packages/drivers/driver-sql/src/schema-drift.remedy-sentence.test.ts +99/-0 (new)",
"packages/drivers/driver-sql/src/schema-drift.ts +39/-4",
"packages/drivers/driver-turso/src/turso-driver.ts +104/-9",
"packages/drivers/driver-turso/src/turso-remote-drift-apply-limit.test.ts +168/-0 (new)",
"total 10 files, +817/-18 against origin/main"
],
"line_budget": "not applicable: no skills/** or governed surface touched (no .claude/, docs/adr/, AGENTS.md or workflows)",
"op_table": [
"drop_column (destructive; emitted by the SQLite differ): REMOTE applied",
"drop_index (destructive; SQLite yes): applied",
"create_index (safe; SQLite yes): applied",
"recreate_index (needs_confirm, or destructive when unique; SQLite yes): applied",
"replace_unique_index (safe; SQLite yes): applied",
"relax_not_null (needs_confirm; SQLite yes): skipped, needs the table rebuild (limit declared)",
"tighten_not_null (destructive; SQLite yes): skipped, needs the table rebuild (limit declared)",
"drop_column_default (safe; SQLite yes): skipped, needs the table rebuild (limit declared)",
"widen_varchar (safe; PostgreSQL/MySQL only): needs the table rebuild",
"narrow_varchar (destructive; PostgreSQL/MySQL only): needs the table rebuild",
"manual_column_type_change (needs_confirm; PostgreSQL/MySQL only): report only on every face (limit null)",
"manual_widen_varchar_to_text (needs_confirm; PostgreSQL/MySQL only): report only on every face (limit null)",
"unbuildable_index (needs_confirm; SQLite yes): report only on every face (limit null, still skipped)"
],
"message_consumers": "The remedy stays in the shared message, with the same bytes on every face (schema-drift.remedy-sentence.test.ts pins the three destructive column messages character for character), so the remote and local reports stay equal. Only the human plan prints the finding without that tail, plus the driver's limit, and only for an entry whose driver declares one. Each consumer: plan human (changed as described); plan --json changes[].message (unchanged; finding 2); apply human listing (unchanged; finding 1); boot [schema-drift] warn (SqlDriver.reconcileAndWarnDrift runs only on the Knex faces, where the apply does rebuild, so its remedy is true); OS_ARTIFACT_URL boot gate (returns on the REMOTE driftReportCaveat before it reads any message); organization-ownership messageOf (only for its own column drops, which the REMOTE face applies; its remote pin is green). REMOTE_DRIFT_REPORT_CAVEAT was re-read and is still true ('applies those and names the rest as not applied'), so it is untouched.",
"red_green": "Red at a512b91 (pins only; production code equals 2e15620): sqld 1 failed / 6 passed, unit 2 failed / 2 passed. Green at d99ee34: sqld 7/7, unit 4/4. Post-merge at 59f489f: sqld 7/7 plus the ceremony 1/1, unit 4/4."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Claim amendment to 6220405114 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T18:44Z. ⛔ Not a new claim; same session, same branchclaude/issue-23925-remote-plan-remedy, PR #24014. This answers the dev's open question in its report (B).Clause-②: yes (widening)
Why the line moves from
no: the line asks whether the card widens an accept set OR the public surface. PR fix(cli,driver-turso): os migrate plan on a Turso remote database lists the changes it cannot apply with the transport's limit, not --allow-destructive #24014 adds two public members:TursoDriver.driftApplyLimit(opType), on@objectstack/driver-turso's exported class;driftFindingWithoutRemedy, a new value export of the@objectstack/driver-sqlentry.No accept set and no
packages/speccontract moves, but the published surface grows.@objectstack/driver-sqland@objectstack/driver-tursogominor. The review owed is atTIER_DEFAULT: the lane seat's own review of record.Surface added:
packages/cli/src/commands/migrate/apply.ts: only the pre-confirmation listing (renderPlan, passed the samedescribethe plan uses) and the deferred-count line ("N destructive change(s) will be SKIPPED (re-run with --allow-destructive …)"). On a Turso remote primary they print the same impossible remedy and count a tightening that a re-run cannot apply either. The dev measured this at the public door,out_of_scope_findings[0]. It is the same family, the seam now exists, and this card is that family's closing card. Declared todomain:cli.packages/cli/src/utils/schema-migrate.tsandpackages/drivers/driver-sql/src/index.ts, as the dev declared in its report: thedriftApplyLimitseam and one re-export line.⛔ Still no
packages/spec/**, and no branch on the dialect, URL or transport name.Generated by Claude Code
test(cli): pin os migrate apply's listing and held-back count against…
fix(cli): os migrate apply's listing and held-back count read the dri…
test(cli): the issuer pin's stub stack declares no apply limit, and i…
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23925,
"status": "done",
"round": "2 (addendum to os-dev-report 6222358607): the seat's answer B and the folded class-a finding",
"branch": "claude/issue-23925-remote-plan-remedy",
"pr": "#24014",
"head": "455c7f2885a262b383054fcbb5780e7acc97d31b",
"session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — subagent run, so the parent's (the domain:engine seat 2 PM) id",
"premise_still_valid": true,
"summary": "(1) Changeset: driver-sql and driver-turso are now minor and cli patch, with 'Clause-②: yes (widening)' on its own line. The operator paragraph is kept and gains two sentences for the apply listing. TursoDriver.driftApplyLimit(opType) and driftFindingWithoutRemedy are named for a driver author. (2) os migrate apply: the listing before its prompt now passes renderPlan the plan's describe. The held-back line 'N destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them).' counts only the destructive entries the database can apply, and the rest go on their own line: 'N destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).' Nothing after the prompt changes, nor the --json document. The describe is now one helper pair in schema-migrate.ts (collectDriftApplyLimits, describeWithApplyLimits) that plan and apply both call; the plan's output is unchanged by that refactor (its pins are green). A driver that declares nothing keeps today's apply output byte for byte, held by a unit CONTROL and by a local-file CONTROL in the sqld file. (3) A pass for the wrong reason was closed: apply.account-issuer-json-refusal.test.ts stubs the stack without the new member, so its human case exited 1 from a TypeError before the pre-flight. The stub now declares no limit, and the case asserts the pre-flight ran. An ablation proves the new assertion can fail. (4) origin/main did not move under any touched file and merges cleanly, so it was not merged.",
"tests": "Red at ca87de9 (pins only; production code equals 59f489f). sqld file apply.remote-face.integration.test.ts: 2 failed / 7 passed, namely the new remote 'apply --yes, human face' case and the listing assertion added to the remote 'apply --allow-destructive, human face' case. Both received '... must be backfilled. Run "os migrate apply --allow-destructive".'; both local-file CONTROLs were green. Unit apply.remote-posture.test.ts: 2 failed / 10 passed, with the CONTROL and the --json equality case green. Green at 48ac513: unit 4 files / 28 tests (apply.remote-posture, plan.apply-limit, schema-migrate.lazy-driver-import, schema-migrate.pending-render); sqld apply.remote-face 9/9, organization-ownership.remote-face 1/1, artifact-boot-migration.unbuildable-index 3/3; apply.account-issuer-json-refusal + test/exit-signal.pin 2 files / 118 tests. Ablation at 455c7f2: the issuer stub's 'driftApplyLimit: () => null,' was deleted through scripts/ablation-replace.mjs (anchor 1 to 0, blob fcd562cd to 7509aeb0) and the human case went red (expected vi.fn() to be called 1 times, but got 0); restored to the HEAD blob with git diff HEAD empty. At 455c7f2: cli typecheck exit 0 (tsc + check:test-typecheck OK); cli unit tier 3 shards 103+102+102 files, 1754+1335+1486 tests, all passed. End to end through bin/run-dev.js on a sqld this round started and stopped by PID: 'migrate apply --yes' exit 0. Its listing gives the tightening and the relax the limit sentence and the drop its --allow-destructive remedy, followed by '1 destructive change(s) will be SKIPPED (re-run ...)' and '1 destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).'",
"gates": "Local at 455c7f2: dispatch-gates --commands --repo objectstack-ai/objectstack gave the same 69 commands as at 59f489f, each with its exit captured before any pipe. --ran reconciliation (':: exit N' lines): 69 derived, 67 run exit 0, 2 NOT-MEASURED (check:dual-build-cjs-loads and check:i18n-coverage, both exit 3 PREREQUISITE NOT MET), 0 UNRUN. Changeset gates, each run with no event, with an event carrying the live body and with an event carrying the replacement body: check-changeset-no-major --base origin/main exit 0 all three. For the replacement body it prints 'LEVEL AXIS: this PR declares clause-② yes (widening), and it grades a package ... at minor or above', naming driver-sql minor and driver-turso minor, with cli patch not refused. For the live body it prints 'declares clause-② no'. With no event the level axis is NOT APPLICABLE. check-adr-0087-registration --base origin/main exit 0 all three: 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'. It takes no --event (grep finds none); it reads the changeset. ESLint over the 6 TypeScript files this round changed: exit 0, no output. CI at 455c7f2, read 19:36Z: every check run completed, 34 success (including the five required: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard) and 6 skipped by their selectors.",
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "1 REST write this round: this addendum, one fleet-relay stroke (POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]) carrying POST /repos//issues/23925/comments. git push (four pushes this round) is not counted. No PR body PATCH and no label write.",
"open_questions": [
{
"question": "Fold in the post-apply held-back line too? After the apply runs, 'Skipped N destructive change(s) (re-run with --allow-destructive to include them).' still counts every destructive entry it held back. That includes a tightening a re-run cannot apply: measured end to end at 455c7f2 on sqld, it read 'Skipped 2' for one drop and one tightening. The amendment confines apply.ts to the listing and the pre-prompt count, so the line is untouched and named in Acceptance note 2.",
"options": [
"A: fold it into this PR. Split heldBack exactly as the pre-prompt count now splits deferred, with the same limits map. That is about 6 lines in apply.ts plus one assertion each in the unit pin and the sqld pin. Cost: one more round and a wider apply.ts surface than the amendment granted.",
"B: leave it, as the amendment ruled. The pre-prompt count already told the operator, before confirming, which held-back entries a re-run cannot apply. Cost: the post-apply line still invites a re-run for that tightening."
],
"recommendation": "A. Real need: it is the same wrong remedy at the same public door, measured, read by the operator who just ran the apply. Long-term soundness: this PR is the family's closing card; leaving one line of the family behind re-opens it, and the seam (limits map) is already in scope. Preventing AI mistakes: an agent reading the run's output acts on its last held-back line, so a contradictory tail sends it to re-run with --allow-destructive for nothing. Startup focus: no new surface, the same helper, a few lines. If the seat holds the amendment's boundary, B stands and the note records it."
}
],
"out_of_scope_findings": [],
"deviations": [
"Test files touched beyond the ones the amendment names: apply.remote-posture.test.ts (its stub stack gains driftApplyLimit, plus the new block) and apply.account-issuer-json-refusal.test.ts (its stub gains the member, plus one assertion that the pre-flight ran). Both are beside apply.ts, and both stub the SchemaStack interface that gained the member; without it the second one passed for the wrong reason.",
"The shared describe moved out of plan.ts into schema-migrate.ts (collectDriftApplyLimits, describeWithApplyLimits) so that plan and apply use one spelling. plan.ts lost the inline copy and its now-unused ManagedDriftEntry type import; the plan pins and the sqld plan cases are green.",
"origin/main was not merged: it moved 6 commits, none under a touched file, and git merge-tree is clean.",
"The driver typechecks and the driver suites were not re-run this round: no driver file changed since 59f489f (typecheck) and d99ee34 (suites).",
"The worktree was recreated on the pushed branch for this round and removed again after the last push (node_modules first, then git worktree remove; clean)."
],
"files_changed_this_round": [
".changeset/23925-remote-plan-remedy.md +8/-6",
"packages/cli/src/commands/migrate/apply.ts +24/-3",
"packages/cli/src/commands/migrate/plan.ts +4/-13",
"packages/cli/src/utils/schema-migrate.ts +39/-0",
"packages/cli/src/commands/migrate/apply.remote-posture.test.ts +103/-1",
"packages/cli/src/commands/migrate/apply.account-issuer-json-refusal.test.ts +4/-0",
"packages/cli/src/commands/migrate/apply.remote-face.integration.test.ts +47/-2",
"branch total against origin/main: 13 files, +1025/-22"
],
"line_budget": "not applicable: no skills/** or governed surface touched",
"red_green_apply_listing": "Red at ca87de9 (production equals 59f489f): sqld 2 failed / 7 passed, unit 2 failed / 10 passed. Green at 48ac513: sqld 9/9, unit 28/28. At 455c7f2: the full cli unit tier passed and CI passed.",
"pr_body_replacement": {
"note": "Replacement text for the sections that change; the seat writes the body (dev does not PATCH it). Every other line of the live body, including the first line 'Fixes #23925' and the footer, stays. The live body's '## Summary' becomes '## What changes'; its '## Evidence' and '## Tests and gates' become '## Verification'. The assembled full body is in the dev's scratch at pr-body-r2.md (16869 bytes, no angle brackets). It drove the --event run above.",
"sections": {
"clause_line": "Clause-②: yes (widening)",
"what_changes": "## What changes\n\nOn a Turso REMOTE primary (
libsql://...), neitheros migrate plannor the listingos migrate applyprints before its prompt offers--allow-destructivefor a drift entry that database cannot apply. The driver that reported each entry declares what its apply door cannot perform, and both commands read that declaration. Neither branches on the dialect, the transport name, the URL or the driver's class.\n\n- The declaration (driver-turso).REMOTE_DRIFT_OP_ANSWERSinturso-driver.tsanswers every drift op the shared differ emits withapplied,needs-table-rebuildorreport-only. Itssatisfies Record(DriftOp type, answer)clause fails the package's typecheck when an op is added toDriftOpand left unclassified, theREMOTE_FACE_ANSWERSpattern. The REMOTEapplyMigrationEntriesdispatches by it: onlyappliedrows are sent, and every other entry is skipped before any DDL and logged with its reason.TursoDriver.driftApplyLimit(opType)answers from the same rows: the limit sentence for aneeds-table-rebuildop,nullfor every other op, andnullfor every op on the local and embedded-replica faces. The commands and the apply door therefore read one object and cannot disagree about an entry.\n- The seam (cli,schema-migrate.ts).\n -SchemaStack.driftApplyLimit(entry)asks the driver that reported the entry. That is thedriftOwnersmapapplyMigrationEntriesalready routes by, read through the optionalSqlDriverLike.driftApplyLimit. A driver that declares nothing has no limit.\n -collectDriftApplyLimits(stack, drift)gathers the entries that have a limit.\n -describeWithApplyLimits(limits)is the one spelling of the line printed under such an entry: its finding, then\"os migrate apply\" cannot apply it on this database: LIMIT., in place of the--allow-destructivesentence. Both commands read it.\n-os migrate plan. An entry with a limit is still listed and counted (it is still drift), under that line. The footer offers--allow-destructiveonly for the destructive entries the database can apply, drops the apply line when it can apply nothing listed, and says how many listed changes remain drift.\n-os migrate apply, the listing before its prompt only. The listing passesrenderPlanthe samedescribe. The held-back countN destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them).now counts only the destructive entries a re-run could apply. The rest are named on their own line:N destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).Nothing after the prompt changes: notnotApplied, not PARTIAL, not the exit code, not the post-applySkipped Nline, and not the--jsondocument.\n- The remedy sentence (driver-sql).tighten_not_null,narrow_varcharanddrop_columnmessages end with anos migrate apply --allow-destructivesentence. That sentence is now spelled once (DESTRUCTIVE_COLUMN_REMEDY), with the same bytes.driftFindingWithoutRemedy(entry)takes exactly that tail off, and only the limited entries use it. Every other reader keeps the message as written.\n- Public surface: the widening the declaration line names.TursoDriver.driftApplyLimit(@objectstack/driver-turso) anddriftFindingWithoutRemedy(the@objectstack/driver-sqlentry) are new and additive, so those two packages gominor.@objectstack/clistayspatch. Nopackages/speccontract and no accept set moves.\n- No declaration, no change. When no entry has a limit, both commands take the old code path: the samerenderPlan(drift)call, the same footer string and the same held-back line. That is every database but a Turso REMOTE one.\n\n### Each drift op, and what the REMOTE face does with it\n\n| op | category | emitted by the SQLite differ | REMOTE apply door |\n|:--|:--|:--|:--|\n|drop_column| destructive | yes | applied |\n|drop_index| destructive | yes | applied |\n|create_index| safe | yes | applied |\n|recreate_index| needs_confirm, or destructive when unique | yes | applied |\n|replace_unique_index| safe | yes | applied |\n|relax_not_null| needs_confirm | yes | skipped: needs the table rebuild |\n|tighten_not_null| destructive | yes | skipped: needs the table rebuild |\n|drop_column_default| safe | yes | skipped: needs the table rebuild |\n|widen_varchar| safe | no (PostgreSQL / MySQL only) | skipped: needs the table rebuild |\n|narrow_varchar| destructive | no (PostgreSQL / MySQL only) | skipped: needs the table rebuild |\n|manual_column_type_change| needs_confirm | no (PostgreSQL / MySQL only) | report only, as on every face |\n|manual_widen_varchar_to_text| needs_confirm | no (PostgreSQL / MySQL only) | report only, as on every face |\n|unbuildable_index| needs_confirm | yes | report only, as on every face |\n\nA report-only op answersnullfromdriftApplyLimit. No face applies it, so its skip is not a limit of this transport, and its finding already names its remedy. The declaration is therefore a per-op answer rather than a set of appliable ops: a plain set would print this transport's rebuild reason against anunbuildable_index.\n\n### Who reads the entry'smessage, and why each one stays truthful\n\nThe message keeps its bytes on every face.schema-drift.remedy-sentence.test.tspins the three destructive column messages character for character. The report stays identical across faces, soturso-remote-drift-detection.test.tskeeps holding the local and remote reports equal. Only the human listings change what they print, and only for an entry whose driver declares a limit.\n\n-os migrate plan(human,renderPlan): changed as above.\n-os migrate apply(human, the listing before the prompt): changed as above.\n-os migrate plan --jsonandos migrate apply --json: unchanged (acceptance note 1).\n- The boot[schema-drift]warning (SqlDriver.reconcileAndWarnDrift): runs only on the Knex faces, where the apply door does rebuild, so its remedy is true.\n- TheOS_ARTIFACT_URLboot gate (artifact-boot-migration.ts): returns on the REMOTE face'sdriftReportCaveatbefore it reads a message.\n-REMOTE_DRIFT_REPORT_CAVEAT: re-read, and still true ("applies those and names the rest as not applied"). Untouched.\n- The organization-ownership ceremony (messageOf): reads messages only for its own column drops, which the REMOTE face applies. Its remote pin is green.","verification": "## Verification\n\nAll of it runs against a real
sqld(libsql-server 0.24.32,libsql://127.0.0.1:PORT?tls=0, archive sha256 checked). The fixture isapply.remote-face.integration.test.ts's own: a hand-madeos23848_notetable with three drifts,legacy_23848(drop_column),code(tighten_not_null) andmemo(relax_not_null). Each remote case has a local SQLite file CONTROL. CI has nosqld, so the unit pins hold the same rules against a stubbed stack whose driver double carries the declaration:plan.apply-limit.test.tsand the new block inapply.remote-posture.test.ts.\n\n### Red, then green: the plan\n\n- Red ata512b914f. That commit holds the pins alone; its production code equals the base2e1562091.\n - sqld: 1 failed, 6 passed. The failure is the remote plan case, withReceived: \"os23848_note.code: metadata declares ... must be backfilled. Run \"os migrate apply --allow-destructive\".\". The local-file CONTROL is green.\n - Unit: 2 failed, 2 passed. The CONTROL and the--jsoncase are green.\n- Green atd99ee347e. sqld 7 of 7, unit 4 of 4.\n\n### Red, then green: the apply's listing\n\n- Red atca87de9ee. That commit holds the pins alone; its production code equals59f489f6d.\n - sqld: 2 failed, 7 passed. The failures are the remoteapply --yeslisting case and the listing assertion in the remoteapply --allow-destructivehuman case, both receiving... must be backfilled. Run \"os migrate apply --allow-destructive\".. Both local-file CONTROLs are green:apply --yeson a file keeps the remedy and2 destructive change(s) will be SKIPPED (re-run ...).\n - Unit (apply.remote-posture.test.ts): 2 failed, 10 passed. The CONTROL and the--jsonequality case are green.\n- Green at48ac51325.\n - sqld: 9 of 9, plusorganization-ownership.remote-face.integration.test.ts1 of 1 andartifact-boot-migration.unbuildable-index.test.ts3 of 3.\n - Unit: 4 files, 28 tests.\n - The new--jsonpin compares the apply's document under a declaring driver with the one a driver that declares nothing gets, key for key and entry for entry.\n\n### A pass for the wrong reason, closed\n\napply.account-issuer-json-refusal.test.tsstubs the stack without the newdriftApplyLimitmember. Its human case ("the human face of the same refusal exits 1") therefore still exited 1, but from a TypeError in the listing, before the pre-flight ran. The stub now declares no limit, and the case asserts the pre-flight ran.\n\n### Ablations\n\n- The driver pin can fail. Atd99ee347e,tighten_not_nullwas changed from'needs-table-rebuild'to'applied'inREMOTE_DRIFT_OP_ANSWERS.\n - The mutation went throughscripts/ablation-replace.mjsin WRAP mode, inside a script with atraprestore, and was proven on disk: anchor 1 to 0, blob1a3a5461to37038760.\n -turso-remote-drift-apply-limit.test.tsfailed 1 and passed 3.\n - The file was restored to the HEAD blob, withgit diff HEADempty.\n- The issuer pin's new assertion can fail. At455c7f288, the stub'sdriftApplyLimit: () => null,was deleted the same way: anchor 1 to 0, blobfcd562cdto7509aeb0.\n - The human case went red withexpected \"vi.fn()\" to be called 1 times, but got 0 times: 1 failed, 3 passed.\n - The file was restored to the HEAD blob, withgit diff HEADempty.\n- No build. Both subjects resolve tosrc/through relative imports.\n\n### End to end through the CLI's own entry\n\nBoth runs went throughtsx bin/run-dev.js, on asqldthis run started and stopped by its PID, with the same three drifts onos23925_note.migrate apply --yesat455c7f288exited 0. The listing before its prompt read:\n\ntext\n Destructive (requires --allow-destructive)\n ✗ os23925_note.code [tighten_not_null]\n os23925_note.code: metadata declares `storage.notNull` but the column is nullable — existing nulls must be backfilled. \"os migrate apply\" cannot apply it on this database: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run.\n ✗ os23925_note.legacy_23925 [drop_column]\n os23925_note.legacy_23925: column exists in the database but not in metadata (orphaned) — \"os migrate apply --allow-destructive\" to drop it.\n ℹ 3 change(s): 0 safe, 1 needs-confirm, 2 destructive\n ⚠ 1 destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them).\n ⚠ 1 destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).\n\n\nmigrate planatd99ee347eexited 0, and its footer read:\n\ntext\n Apply with: os migrate apply (add --allow-destructive for the 1 destructive change(s) it can apply on this database)\n ⚠ 2 change(s) above cannot be applied on this database by \"os migrate apply\" (each says why), so they remain drift after it runs.\n\n\n### Tests and gates\n\nRecorded at455c7f288unless a line says otherwise.\n\n- Typecheck.\n -@objectstack/cli(tsc --noEmit, thencheck:test-typecheckOK): exit 0 at455c7f288.\n -@objectstack/driver-tursoand@objectstack/driver-sql: exit 0 at59f489f6d. No driver file has changed since.\n-@objectstack/cliunit tier. 3 shards: 103 + 102 + 102 files and 1754 + 1335 + 1486 tests, all passed.\n-@objectstack/cliintegration, run locally.\n -apply.remote-face.integration.test.ts: 9 of 9, onsqld.\n -organization-ownership.remote-face.integration.test.ts: 1 of 1, onsqld.\n -artifact-boot-migration.unbuildable-index.test.ts: 3 of 3.\n - These three ran at48ac51325;455c7f288changes one unit test file.\n - At59f489f6d:plan.telemetry-sibling,plan.deferred-readsandplan.reads-env-files, 3 files, 10 tests.\n - The rest of the tier is CI's.\n- The driver suites, run atd99ee347e. No driver file has changed since.\n -@objectstack/driver-turso: 92 files, 2429 passed, 33 skipped.\n -@objectstack/driver-sql: 223 files passed and 11 skipped; 3677 tests passed and 208 skipped.\n- Derived gate families.dispatch-gates --commands --repo objectstack-ai/objectstackgives 69 commands, the same 69 as at59f489f6d. Each exit code was captured before any pipe.\n ---ranreconciliation: 69 derived, 67 run with exit 0, 2 NOT MEASURED, 0 UNRUN.\n - The two NOT MEASURED arecheck:dual-build-cjs-loads(packages outside this build closure have nodist/) andcheck:i18n-coverage(a partial round). Both exited 3, PREREQUISITE NOT MET.\n- The changeset gates, each run three ways (no event, an event carrying the live body, and an event carrying this body):\n -check-changeset-no-major.mjs --base origin/mainexits 0 every time, with nomajor.\n - With this body:LEVEL AXIS: this PR declares clause-② yes (widening), and it grades a package ... at minor or above, naming@objectstack/driver-sql: minorand@objectstack/driver-turso: minor, with@objectstack/cli: patchnot refused.\n - With the live body:this PR declares clause-② no.\n - With no event: the level axis is NOT APPLICABLE (nopull_request).\n -check-adr-0087-registration.mjs --base origin/mainexits 0 every time:this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen). It reads the changeset, not the event.\n- Also run. ESLint--no-inline-configover the 6 TypeScript files this round changed: exit 0, no output.\n- CI at455c7f288. Every check run completed: 34 success, including the five required contexts (Lint & Repo Gates,TypeScript Type Check,Test Core,Build CoreandGoverned Surface Queue Guard), and 6 skipped by their selectors.","file_surface": "## File surface\n\nThe claim's surface, and its amendment 6222387090:\n\n-
packages/cli/src/commands/migrate/apply.ts: only the listing before the prompt and the held-back count line.\n-packages/cli/src/utils/schema-migrate.ts: thedriftApplyLimitseam, the two shared helpers, and an optionaldescribeargument torenderPlan.\n-packages/drivers/driver-sql/src/index.ts: one re-export line.\n- Tests beside each:\n -plan.apply-limit.test.ts;\n -apply.remote-posture.test.ts, whose stub stack gains the member, plus a new block;\n -apply.account-issuer-json-refusal.test.ts, whose stub gains the member, plus one assertion;\n -apply.remote-face.integration.test.ts, which holds the sqld fixture;\n -schema-drift.remedy-sentence.test.ts;\n -turso-remote-drift-apply-limit.test.ts.\n\nNothing underpackages/spec/**, and no published driver contract (packages/spec/src/contracts/**) gains a member. Neither command readsdialect, the transport, the URL or a class name.","acceptance_notes": "## Acceptance notes\n\n1. The
--jsondocuments are unchanged. Inos migrate plan --json,changes[].messagekeeps the shared remedy, and no field marks an entry the database cannot apply.os migrate apply --jsonis pinned equal to what a driver that declares nothing produces. Adding a mark widens a document, and no consumer of either JSON document was measured to need one.\n2. The post-apply held-back line. After the apply runs,Skipped N destructive change(s) (re-run with --allow-destructive to include them).still counts every destructive entry it held back. On a remote primary that includes a tightening a re-run cannot apply: measured end to end above, it readSkipped 2with one drop and one tightening. The amendment confinesapply.tsto the listing and the pre-prompt count, so this line is left as it is.\n3.drop_column_default. Its message carriesrun \"os migrate apply\"mid-sentence rather than as a tail. On a remote primary, the listing line therefore shows that sentence with the limit after it. That entry needs a literal runtime-token DEFAULT written by an old build, so a database the current sync created does not reach it.\n4. The REMOTE apply door's log. A skipped rebuild entry now reads... skipped: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run, from the declaration's sentence. It used to endwhich this transport does not run. A report-onlyunbuildable_indexon that face is now logged as skipped; it used to be skipped with no log line. No test pins either log line."}
}
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Claim amendment 2 to 6220405114 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T19:39Z. ⛔ Not a new claim; same session, same branchclaude/issue-23925-remote-plan-remedy, PR #24014. This answers the dev's round-2 open question (A).packages/cli/src/commands/migrate/apply.ts, the post-apply held-back line ("Skipped N destructive change(s) (re-run with --allow-destructive to include them).").notApplied, not the exit code, not--json.sqldat455c7f288("Skipped 2" for one drop and one tightening). This card is the family's closing card.apply.tswhose stubs theSchemaStackmember reaches (apply.remote-posture.test.ts,apply.account-issuer-json-refusal.test.ts), as the dev declared.Clause-②: yes (widening)stands (amendment 6222387090).Generated by Claude Code
test(cli): pin os migrate apply's post-apply held-back line against t…
objectstack-fleet commentedon Oct 16, 2026
Claim amendment 3 to 6220405114 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T19:45Z. ⛔ Not a new claim; same branch, PR #24014.content/docs/deployment/cli.mdx, one caveat under the migrate category table (:1006-1012) only, declared todomain:devx. That table namesos migrate apply --allow-destructiveas what applies a NOT NULL tightening. On a Turso remote primary the apply cannot (no SQLite table rebuild, since fix(cli,driver-turso): os migrate plan/apply run on a Turso remote database and name the drift entries they cannot apply #23923), and after this PR the plan and the apply say so. The caveat names the entries that database leaves as drift (a tightened or relaxed NOT NULL, a dropped DEFAULT), and says that plan and apply mark them.Clause-②: yes (widening)stands (amendment 6222387090).Generated by Claude Code
fix(cli): os migrate apply's post-apply held-back line splits by the …
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23925,
"status": "done",
"round": "3 (addendum to 6223275472): the seat's answer A (the post-apply held-back line) and claim amendment 3 (the CLI docs caveat)",
"branch": "claude/issue-23925-remote-plan-remedy",
"pr": "#24014",
"head": "512414eef",
"session": "session_01TQLy3Yq6vF4SDpTXdr3Uwi — subagent run, so the parent's (the domain:engine seat 2 PM) id",
"premise_still_valid": true,
"summary": "(1) apply.ts, the post-apply line only. 'Skipped N destructive change(s) (re-run with --allow-destructive to include them).' now counts only the held-back entries a re-run could apply here. The rest get their own line: 'Skipped N destructive change(s) that a re-run could not apply on this database either: they remain drift (each says why above).' The split uses the same limits map as the pre-prompt count. That map is now computed once, before the listing, for the human face only; under --json it is an empty map and stack.driftApplyLimit is not called. notApplied, PARTIAL, the exit code and --json are unchanged. With no limits the line is byte-identical: heldBack = skipped held-back entries, the old skipped.length - notApplied.length. (2) content/docs/deployment/cli.mdx: one caveat under the migrate category table and nothing else on the page: 'On a Turso remote primary (
libsql://), a tightened or relaxedNOT NULLand a dropped DEFAULT stay drift, because that transport runs no SQLite table rebuild.os migrate planandos migrate applylist each such entry with\"os migrate apply\" cannot apply it on this database: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run., offer--allow-destructiveonly for the destructive changes that database can apply, and count the rest as drift a re-run cannot apply.' (3) The changeset's operator sentence about held-back counts now covers both lines ('before the prompt and after the run'). The grades are unchanged: driver-sql and driver-turso minor, cli patch, 'Clause-②: yes (widening)'. (4) origin/main was not merged: no conflict (merge-tree clean), and nothing under a touched file moved.","tests": "Red at db6056e (pins only; production code equals 455c7f2). Unit apply.remote-posture.test.ts: 1 failed / 11 passed. The limited case expected two post-apply lines and received [' ⚠ Skipped 2 destructive change(s) (re-run with --allow-destructive to include them).']; its CONTROL ('Skipped 2', unchanged) was green. sqld apply.remote-face.integration.test.ts: 1 failed / 8 passed. The remote 'apply --yes, human face' case received the same single 'Skipped 2' line; the local-file CONTROL ('Skipped 2') was green. Green at 512414e, through os-verify-lock: cli typecheck exit 0 (tsc + check:test-typecheck OK); unit 4 files / 23 tests (apply.remote-posture, apply.account-issuer-json-refusal, plan.apply-limit, schema-migrate.lazy-driver-import); sqld apply.remote-face 9/9 plus organization-ownership.remote-face 1/1. End to end through bin/run-dev.js on a sqld this round started and stopped by PID: 'migrate apply --yes' at 512414e exit 0. After the run it prints 'Skipped 1 destructive change(s) (re-run with --allow-destructive to include them).' then 'Skipped 1 destructive change(s) that a re-run could not apply on this database either: they remain drift (each says why above).', and notApplied still names the relax. The pins assert one post-apply line set for the limited case and one for its CONTROL in each file, so two assertions per file rather than one; the CONTROL was asked for alongside.",
"gates": "Local at 512414e. dispatch-gates --commands --repo objectstack-ai/objectstack: 99 commands (the 69 from before plus 30 the docs path adds), each with its exit captured before any pipe. --ran reconciliation (':: exit N'): 99 derived, 97 run exit 0, 2 NOT-MEASURED (check:dual-build-cjs-loads and check:i18n-coverage, both exit 3 PREREQUISITE NOT MET), 0 UNRUN. check:skill-examples first exited 3 (client-react not built); after building the client SDK closure (35/35 cache hits) it exited 0 (263 prose examples type-check across 3 surfaces), and that is the exit recorded. pnpm check:doc-authoring exit 0. check-changeset-no-major --base origin/main: exit 0 with no event, and exit 0 with an event carrying the final body ('LEVEL AXIS: this PR declares clause-② yes (widening) ... at minor or above'; driver-sql minor, driver-turso minor, cli patch not refused). check-adr-0087-registration --base origin/main: exit 0 ('adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'). CI at 512414e, read 20:17Z: 34 success, including Build Docs and four of the five required (Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard). 5 skipped by their selectors. Test Core (3/6) was still in progress, so the Test Core roll-up had not reported. Not waited on.",
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "1 REST write this round: this addendum, one fleet-relay stroke (POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]) carrying POST /repos//issues/23925/comments. git push (two pushes this round) is not counted. No PR body PATCH and no label write.",
"open_questions": [],
"out_of_scope_findings": [],
"deviations": [
"Two assertions per pin file rather than one: the limited case and its CONTROL (a driver that declares nothing reads 'Skipped 2 ...' byte for byte), which the round also asked for.",
"The changeset's operator paragraph was edited by one sentence ('Its counts of held-back destructive changes, before the prompt and after the run, offer ...'), so the release text stays true for the post-apply line. The grades and the Clause-② line are unchanged.",
"The limits map moved out of the listing block, so the post-apply line can read it. It is built only for the human face; under --json it is an empty map, and the --json path does not call stack.driftApplyLimit.",
"Not re-run this round: the full cli unit tier (last run at 455c7f2; since then the diff changes only the post-apply line and two test files, and the pins reaching it ran green), the driver typechecks and the driver suites (no driver file changed).",
"The worktree was recreated on the pushed branch and removed again after the last push."
],
"files_changed_this_round": [
"packages/cli/src/commands/migrate/apply.ts +21/-8",
"packages/cli/src/commands/migrate/apply.remote-posture.test.ts +8/-0",
"packages/cli/src/commands/migrate/apply.remote-face.integration.test.ts +9/-0",
"content/docs/deployment/cli.mdx +2/-0",
".changeset/23925-remote-plan-remedy.md +1/-1",
"branch total against origin/main: 14 files, +1058/-23"
],
"line_budget": "not applicable: no skills/** or governed surface touched; the docs change is one paragraph in content/docs/deployment/cli.mdx, nothing under content/docs/releases/",
"red_green_post_apply_line": "Red at db6056e (production equals 455c7f2): unit 1 failed / 11 passed, sqld 1 failed / 8 passed; both CONTROLs green. Green at 512414e: unit 4 files / 23 tests, sqld 9/9 plus the ceremony 1/1.",
"pr_body_final": {
"path": "/tmp/claude-0/-home-user/c5f79d24-82a0-5900-a5c3-c8ad369dbef3/scratchpad/issue-23925/pr-body-r2.md",
"bytes": 19275,
"note": "The full final body, written for the seat to set from that file (the dev does not PATCH it). Its sections: the 'Clause-②: yes (widening)' line; '## What changes' (the post-apply line and the docs caveat sentence added); '## Verification' (the post-apply red-to-green leg, the end-to-end excerpt at 512414e with both held-back lines, and Tests and gates at 512414e); '## File surface' (content/docs/deployment/cli.mdx and amendments 6223299622 and 6223389541 added); '## Acceptance notes' (the post-apply note removed; 3 notes remain). No angle brackets, no control bytes, one closing keyword ('Fixes #23925' on line 1), and 'Clause-②' appears once."
}
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #24014 comment 6224004810 ·
domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T20:23Z. It is also the review of record forClause-②: yes (widening). Landing next: ready, then auto-merge through the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landing record: PR #24014 →
c4111c676a·domain:engineseat 2 ·session_01TQLy3Yq6vF4SDpTXdr3Uwi· 2026-10-16T20:56Zmerged: true, merge commitc4111c676a, through the merge queueorigin/mainc4111c676isorigin/main's tip.turso-driver.tscarriesREMOTE_DRIFT_OP_ANSWERS/driftApplyLimit. Thedriver-sqlentry exportsdriftFindingWithoutRemedy.content/docs/deployment/cli.mdxcarries the Turso remote caveat..changeset/23925-remote-plan-remedy.mdis present (minorfor the two drivers,Clause-②: yes (widening))gh-readonly-queue/main/*pr-24014*head remainsState change: this card closed
completedbyFixes #23925;pm:dispatchedremoved in this act. No other card closed with it (closed since 2026-10-16T20:54Z: this card only).Carried: the three
Acceptance notesstay noted, with no carrier: the--jsondocuments carry no limit mark,drop_column_default's mid-sentence remedy, and the REMOTE apply door's log wording.Generated by Claude Code