Skip to content

fix(core): os migrate resume completes a recorded-by run that committed a chunk or used a non-default --chunk-size (#21528) - #21554

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21528-resume-plan-identity
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21528-resume-plan-identity

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21528

Clause-②: no

What changed

runMigrationJournal (packages/core/src/utils/migration-journal.ts) recomputed a resumed run's chunk plan from the rows load() returns at resume time, at the resumed plan's chunk size, and refused PLAN_CHANGED when that plan's hash differed from the one run_started recorded. A resume now reads the chunk plan back from run_started, which has carried it since the runner's first commit (ADR-0119 D2 item 2: "carrying the plan hash and chunk plan"):

  • Identity, the one place the journal hashes. hashMigrationPlan is unchanged. On a resume it hashes the RECORDED chunk boundaries with the plan's declared id and step names, so it compares the plan against what the run started over. The run's chunk size comes back from the journal. A plan whose id or steps changed still refuses PLAN_CHANGED.
  • Rows. Per step, with N rows started over and K of them in committed chunks: a load() that returns N rows binds positionally, as before. One that returns exactly N − K rows (it selects only the remaining work, as recorded-by's does) binds those rows, in order, to the chunks not yet committed. Any other count refuses PLAN_CHANGED and names the step. Every resume that passed before binds exactly as before (N rows, the same boundaries).
  • Unwind after such a resume. A chunk an earlier process committed, whose rows load() no longer returns, cannot be compensated. The unwind compensates this process's chunks newest-first, then halts with run_failed at that chunk with a reason. It does not hand compensate() other rows and journal a clean unwind.
  • A run_started with no recorded chunk plan (only a hand-written journal) keeps today's check: reproduce the recorded hash from the current rows.

No published member is added. MigrationPlan, MigrationPlanStep, MigrationJournalEvent and the run_started payload keep their shapes. MigrationPlanStep.load and RunMigrationJournalOptions.chunkSize gain TSDoc for what a resume does with them. The plan (recorded-by-sentinel.ts), os migrate resume's source and the list mode's resumable are untouched. No new error code: both new refusals are PLAN_CHANGED, the code the same inputs drew before.

The public door, before and after

packages/cli/src/commands/migrate/resume.recorded-by.integration.test.ts now makes three more interrupted runs the way a crash does (a child process runs the recorded-by plan under the real runner and is SIGKILLed inside a chunk's transaction) and drives the real os migrate resume --json:

  • (a) 203 sentinel rows, killed in chunk 1 after chunk 0 committed. The list says resumable: true, committedChunks: [0], unknownChunks: [1]. Before (core built from 1ac7308d7a): --run RUN_ID --yes exited 1 with Refused (PLAN_CHANGED): ... plan hash 037ebfcc70ee54096b8eb2a6aed8cd49 does not match the journal's f36863e5fee4bb4e40093c66a0b3096f. After: exit 0, completed, 2 of 2 chunks, no row left holding the sentinel, chunk_started indices [0, 1, 1] (chunk 0 is not run again).
  • (b) 3 rows started at chunk size 2, killed in chunk 0. The list says resumable: true. Before: exit 1, PLAN_CHANGED. After: exit 0, completed, chunksTotal: 2 (the journal's size; the registered plan's default 200 would make one chunk).
  • (c) The control. A run started by a plan whose step had another name: exit 1, Refused (PLAN_CHANGED), before and after. The sentinel rows and the journal are untouched.

Before the fix that file read 2 failed / 7 passed (the two acts above); after, 9 passed.

Tests (at 14e5287fa8, this branch's head)

  • packages/core/src/utils/migration-journal.test.ts: 29 passed (23 before + 6 new: a shrinking load resumes after a committed chunk; a non-shrinking load resumes positionally from a runner-written journal; the journal's chunk size wins over the plan's; the changed-plan control, three ways (step renamed, plan id changed, step added), each refused with code: 'PLAN_CHANGED' and zero journal writes; a row count that is neither N nor N − K is refused, naming the step; the unwind halt). The crash helper runs the REAL runner and stops a forward inside its chunk, so every resume reads a journal the runner wrote.
  • packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts: 8 passed (1 new: the real plan, started at size 2 and killed after chunk 0 committed, resumes with the plan the owner registers at its default size, 2 of 2 chunks).
  • The door file above: 9 passed (--project integration, run locally because this diff edits that file).
  • Typecheck: @objectstack/core (with check:test-typecheck: 4 files / 4 errors held, unchanged), @objectstack/metadata-protocol, @objectstack/cli (test layer: 3 files / 28 errors held, unchanged), all exit 0.
  • Full suites on ae27f00812 (this diff before the merge of main, which touched none of these files): @objectstack/core 79 files / 2220 tests passed; @objectstack/metadata-protocol 205 files passed, 3 skipped / 3152 tests passed, 19 skipped. @objectstack/cli's unit tier: only the tier-partition pin (test/vitest-tiers-partition.test.ts, 22 passed); this diff changes no CLI source.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands on 14e5287fa8 derived 67 commands; all 67 ran, reconciled with --ran (each line carrying its exit code): 0 NOT MEASURED, 66 exit 0, and one exit 1, node scripts/check-empty-changeset.mjs --base origin/main, explained in the next section. Four roster gates the derivation flags as sharing a directory with these paths also ran green: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity.
  • Lint, narrowed: the population is the 4 changed .ts files, none ignored by eslint.config.mjs. eslint --no-inline-config --format json read 4 files, 0 errors, 0 warnings. That config never enables type-aware linting (no parserOptions.project), so this diff cannot move the verdict of any file it does not touch. The repo-wide pnpm lint is CI's.

Reverse verification and ablation

  • Reverse verification, with the fix committed: migration-journal.ts restored to 1ac7308d7a in the working tree only, blob df8d8d5009 checked equal to the base's, then core rebuilt and node scripts/ablation-dist-preflight.mjs @objectstack/core planResumedRun --absent passed. Red as predicted: core unit 4 failed / 25 passed (the four resume pins; the control and the positional-resume pin stay green, as they should on both trees), the plan pin 1 failed / 7 passed, the door 2 failed / 7 passed (a and b; the control green). Restored with git checkout HEAD -- under an EXIT/INT/TERM trap, proven by the blob (361d75e7ee == HEAD) and an empty git diff HEAD, then rebuilt, with the preflight in default mode showing the marker back in 4 built files and a clean tree.
  • Ablation of the unwind guard, which reverse verification cannot isolate (the old runner refuses before reaching it): node scripts/ablation-replace.mjs planted the naive positional fallback (rowsByChunk.get(c.index) ?? rowsByStep[...].slice(offset, offset + length)). The landing was shown by the anchor count 1 → 0 and the blob change. The unwind pin went red: expected 'compensated' to be 'failed', which is the run handing chunk 0 other rows and journalling a clean unwind. Restored by the tool: blob == HEAD, git diff HEAD empty. The core unit suite imports the runner by relative path, so no build leg applies.

The pending #21498 changeset: a correction to confirm

This PR changes .changeset/21498-cli-compose-migration-recovery.md, which it did not add. That note's "Still refused" bullet said the runner refuses these two kinds of run with PLAN_CHANGED. This change makes that false, and both notes are still pending, so they would ship in one release. The bullet now says these runs reach the runner too and points to the @objectstack/core entry for #21528. check-empty-changeset stays red on this by design: it is the gate's DELIBERATE CORRECTION class, and its remedy is to say so here and get the correction confirmed. Restoring the old bullet would publish a sentence this PR makes false. Please confirm the correction.

The new changeset (.changeset/21528-core-resume-started-over-plan.md) is an @objectstack/core patch with Clause-②: no. No member is added to a published contract. Resume now accepts the runs its list mode already advertises as resumable, as ADR-0119 D2 item 5 declares.

Acceptance notes

  • The list's resumable is plan presence only (resume.ts: Boolean(plans?.get(r.planId))). So a run whose plan genuinely changed, or whose rows moved, is still listed resumable: true and then refused. The triage ruling keeps the list's wording out of this card. I read this from source; I did not measure the list for the control run.
  • A forward resume skips compensated chunks. The forward loop skips every chunk with a chunk_done, and a chunk that was committed and then compensated has one. So resuming forward a run whose in-run unwind failed partway would skip the chunks that unwind had undone. For recorded-by (a shrinking load()), that run's row count matches neither binding, so it is refused PLAN_CHANGED, as it was before. Only a plan whose load() does not shrink would take the skip, and no such plan is registered on this tree. I read this from source and did not measure it. Carrier: none.

Generated by Claude Code

claude added 7 commits October 3, 2026 05:20
…a committed chunk, or started at another chunk size

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
… it started over

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…ing load, the journal's chunk size, the changed-plan control

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…mmitted chunk at a non-default chunk size

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…e() over no rows

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…till-refused bullet it makes false

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

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

Coarse fallback — 27 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 fd5a1cd5973983bf8b1ad69a10148a85c74c9137 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json fd5a1cd5973983bf8b1ad69a10148a85c74c9137

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 14e5287fa80db1806b55bcf4923f14c8bd18de56
Local-runs: none

PR #21554 for card #21528, branch claude/issue-21528-resume-plan-identity, read at 2026-10-03T06:37Z. Net diff against main (merge base 24dc7c1134): 6 files, +647 / -79 — one source file (packages/core/src/utils/migration-journal.ts), three test files, one new changeset, one rewritten bullet in a pending changeset. Inputs: the card body and its four comments (triage 5965275807, unlock 5965394174, claim 5965820808, os-dev-report 5966314554); the PR body, its file list and the net diff; the check-runs on the head, plus the Check Changeset job's steps and its failed step's log. Judged against triage 5965275807 as unlocked by 5965394174. Nothing was built, run or re-run locally.

① Derived judgments

Public surface of @objectstack/core (src/index.ts re-exports utils/migration-journal.js): no exported type, signature, class member or error code moves. The exported declarations the diff touches change in TSDoc only — MigrationPlanStep.load (what a resume does with its return), RunMigrationJournalOptions.chunkSize (ignored on a resume whose journal carries a chunk plan), hashMigrationPlan and runMigrationJournal. MigrationJournalRefusal.code stays string; both new refusal messages carry PLAN_CHANGED, the code this site already drew. MigrationJournalEvent and the run_started payload (detail.chunks of {i, step, offset, length}, which the runner has written unchanged since its first commit) keep their shape — the fix reads back a field the runner already writes. Everything else is module-private: LoadedPlan.rowsByStep becomes rowsOf; loadPlan splits into loadRows, planNewRun, planResumedRun; recordedChunks, sliceOf and UnwindArgs.rowsOf are new or retyped but unexported. Judged RIGHT; Clause-②: no survives this diff.

Accept-set of runMigrationJournal / resumeMigrationJournal on a resume, each change named and judged:

  1. Identity. The resume hashes plan.id, the step names and the chunk boundaries run_started recorded; recordedChunks rebuilds each chunk's stepIndex from its recorded step name, so an unchanged plan reproduces the stored hash exactly and a renamed step maps to -1 and cannot. hashMigrationPlan is byte-unchanged; the one place that fed it on a resume (loadPlan) is the one place changed — the ruling's "measures where the journal hashes today and changes that one place". RIGHT, within "compares what the run started over, not what load() returns now … its declared plan identity and parameters, not its current rows".
  2. Chunk size. A resume runs the recorded chunk plan, so the size comes back from the journal; options.chunkSize and the registered plan's own chunkSize are ignored on such a resume, and the TSDoc says so. resume.ts passes no option and is untouched. RIGHT, within "the run's chunk size comes back from the journal, ⛔ not the current default".
  3. A changed plan. Plan id changed, a step renamed, a step added: PLAN_CHANGED with zero journal writes (core control three ways; door control). RIGHT, within "a genuinely changed plan still refuses PLAN_CHANGED".
  4. Row binding — the new rule. Per step, N is the rows its recorded chunks cover and K the rows in its chunk_done chunks. load() returning N rows binds positionally, so every resume that passed before binds exactly as before; returning exactly N − K binds those rows, in order, to the uncommitted chunks (the shrinking shape recorded-by has); any other count refuses PLAN_CHANGED naming the step, and a recorded chunk of a step the plan does not declare is refused too. Judged WITHIN the ruling and RIGHT: identity is decided before and apart from the rows (rows 1–3); the binding is the mechanism "make resume work" needs for a load() that shrinks by design; it is one rule for every plan with no new member, which is the ruling's stop condition; and every count it refuses was refused at the base too (a count other than N recomputes different boundaries, so the old hash mismatched; a count of N was and is positional). Two residual ambiguities, named, neither introduced by this diff: (4a) a shrinking load() that gained exactly K rows under the interrupted run reads as N and binds positionally — pre-existing, since the same count gave the same recomputed hash at the base; (4b) a non-shrinking load() that lost exactly K rows now reads as N − K and binds in order, with rows of committed chunks possibly forwarded again at attempt: 1, where the base refused by hash accident. No non-shrinking plan is registered on this tree (recorded-by, the only registered plan, shrinks), the new load TSDoc states both readings to a plan author, and telling the two apart needs a plan-declared load shape — a published member, the 强制条款② work the ruling told the dev to stop at. Escalated in ③; not a FAIL.
  5. Fallback. A run_started with no readable recorded chunk plan (a journal this runner did not write, or a malformed detail) keeps the base behaviour: recompute from the current rows and compare the hash. RIGHT; conservative.
  6. The unwind stop. After an N − K-bound resume, rowsOf answers undefined for a chunk an earlier process committed. The unwind compensates this process's chunks newest-first with their own rows, then halts at that chunk with run_failed carrying {phase: 'compensate', reason, step, cause} — the event and detail shape the existing no-compensate() halt already uses — and returns failed. Judged WITHIN the ruling and RIGHT: no new event kind, field or code; ADR-0119 D2 item 4 ("a compensation failure journals and halts loudly; it is never swallowed"); the dev's ablation of the naive positional fallback shows the alternative hands compensate() other rows and journals a clean unwind, the exact harm. Consequence named: such a run stays listed interrupted (one committed chunk outstanding) and a later resume of it is refused PLAN_CHANGED, its count being neither N nor N − K — the aftermath the no-compensate() halt already had, with --apply as the recovery. For an onCrash: 'compensate' plan (none registered; recorded-by declares resume) the same halt replaces the base's false PLAN_CHANGED.
  7. Untouched, as ruled. recorded-by-sentinel.ts (its test only), resume.ts source, the list mode's resumable. RIGHT, "make resume work, not the list quieter".

The ruling's three pins are present and run the real runner: migration-journal.test.ts +6 (shrinking resume after a committed chunk; non-shrinking positional resume; the journal's size over the plan's; the control three ways with zero writes; the neither-count refusal naming its step; the unwind halt with a reason and no error); recorded-by-sentinel.test.ts +1 (the real plan started at size 2, killed after chunk 0, resumes with the owner's default-size plan, 2 of 2 chunks); the door resume.recorded-by.integration.test.ts +3 runs driven through the real os migrate resume --json (203 rows killed in chunk 1 resumes to completed with chunk_started [0, 1, 1] and no sentinel left; 3 rows at size 2 resumes with chunksTotal: 2; the renamed-step control exits 1 Refused (PLAN_CHANGED) with rows and journal untouched). The base header's "this file does not pin around it" note is replaced by those pins. Their verdicts belong to the head's Test Core shards, pending at my read (③).

② Semver level

  • .changeset/21528-core-resume-started-over-plan.md — '@objectstack/core': patch, Clause-②: no. RIGHT: a bug fix in a released package takes patch (never none, never skip-changeset); no export, member or error code is added. The PR body's Clause-②: no line is present and the gate read it (check-changeset-no-major: "this PR declares clause-② no"). The no is the pull-back-to-the-declared-contract kind and cites its text: ADR-0119 D2 item 5 ("resume forward from the first chunk lacking chunk_done") and item 2 (run_started "carrying the plan hash and chunk plan"). The refusal removed is the mis-refusal of an unchanged plan; the deliberate refusal of a changed plan is kept — no, not yes. The changeset body's four claims (identity; the size from the journal; rows bound as N or N − K, else refused naming the step; the unwind halting failed) each match the diff.
  • packages/cli and packages/metadata-protocol — test files only; they publish nothing and owe no changeset. RIGHT.
  • Clause-②: no — confirmed on the diff (① surface row).

The DELIBERATE CORRECTION red — Check Changeset, job 111145179943, on this head:

  • The job's steps: 1–8, 11, 13, 14 and 15 success; 9 and 10 skipped; step 12, "Reject an empty-frontmatter changeset added by this PR", is the only red step. Its log: rule 1 passed ("No empty-frontmatter changeset introduced by this diff"); the exit 1 is rule 2, the foreign-changeset refusal of scripts/check-empty-changeset.mjs, naming exactly .changeset/21498-cli-compose-migration-recovery.md as "present on the merge base and CHANGED by this PR" and printing the COLLISION / DELIBERATE CORRECTION two-class text. Step 13 ("2 non-breaking changeset(s)") and step 15 (no major; clause-② read) are green.
  • The corrected note: .changeset/21498-cli-compose-migration-recovery.md, an @objectstack/cli patch, pending, not added by this PR. One bullet is rewritten; every other byte of the note is identical to the merge base.
  • The base bullet, sentence by sentence against this diff. (B1) "Still refused: a recorded-by run that had committed a chunk before it was interrupted, or that was started with a non-default --chunk-size." — FALSE once this diff lands: door runs (a) and (b) resume to completed. (B2) "resume now reaches the runner for these runs, and the runner refuses them with PLAN_CHANGED." — the first clause stays true; the second is FALSE under this diff. (B3) "Re-running os migrate recorded-by --apply converts whatever rows still hold the sentinel." — still true as a fact, no longer the recovery this note needs; dropping it publishes nothing false.
  • The head bullet, sentence by sentence. (H1) "A run that had committed a chunk, or that was started with a non-default --chunk-size, reaches the runner too." — TRUE: the reach is PR fix(cli,metadata-protocol): os migrate resume completes an interrupted recorded-by run, and os serve reports interrupted migration runs at boot #21527's composition, untouched here, and the door drives both run kinds through the real command to the runner. (H2) "The runner fix that lets it resume is in the @objectstack/core entry for [finding] os migrate resume lists an interrupted recorded-by run as resumable: true, then refuses it PLAN_CHANGED when the run had committed a chunk or used a non-default --chunk-size #21528." — TRUE: that entry is .changeset/21528-core-resume-started-over-plan.md, an @objectstack/core patch naming [finding] os migrate resume lists an interrupted recorded-by run as resumable: true, then refuses it PLAN_CHANGED when the run had committed a chunk or used a non-default --chunk-size #21528, and the only source file in this diff is packages/core/src/utils/migration-journal.ts.
  • Both notes are pending and ship in one release. The correction is confirmed. The red is by design on this head — its cause is the one step and the one file named above — and the gate itself is not changed.

③ Boundary flags

  • open_questions[0] — A, B or C on the 21498 bullet: A. The rewritten bullet is confirmed above sentence by sentence. B would republish B1 and B2, which this diff makes false. C loses the cross-reference a reader of the cli CHANGELOG needs to find the core entry.
  • out_of_scope[0] — the list's resumable is plan presence only (resume.ts: Boolean(plans?.get(r.planId))), so a run whose plan genuinely changed is still listed resumable: true and then refused. ANSWERED: out of this card by the ruling ("not the list quieter"), and the control shows the act refusing loudly, so nothing is silent. ESCALATED as a follow-up candidate for the engine lane, not a condition on this head: with the chunk plan now read back from run_started, the list could compare the registered plan's identity against the journal without a load(); recordedChunks is module-private, so that is a surface question for triage, not a rider here. Carrier on this PR: none.
  • out_of_scope[1] — the forward loop skips every chunk with a chunk_done, a compensated-then-resumed chunk included, so a non-shrinking plan whose in-run unwind failed partway would skip undone work. ANSWERED: pre-existing (the skip line is unchanged by this diff), and for recorded-by such a run is refused PLAN_CHANGED, its count being neither N nor N − K. ESCALATED to triage as a reading question on ADR-0119 D2 item 5 ("the first chunk lacking chunk_done" against a chunk whose compensated undid it); no registered plan reaches it. Carrier on this PR: none.
  • Reviewer's own, from ① row 4b — the N − K binding cannot tell a shrinking load() from a non-shrinking one that lost exactly K rows; the honest remedy is a plan-declared load shape, a published member, which is Clause-② work and was correctly not taken here. ESCALATED to the engine lane as a card candidate; not a condition on this head.
  • Pending at my read (the reading time above), named and not judged: Lint & Repo Gates, Type Check · workspace, Test Core 1/6 through 6/6, Dogfood Regression Gate 1/3 and 3/3, Temporal Conformance (live PG + MySQL). Green at my read: Build Core, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Governed Surface Queue Guard, Dogfood Regression Gate (2/3), Dogfood Verify CLI, Check PR Size, Check Documentation Links, Auto Label, filter, Flag docs affected by code changes, and the three claim and part-of guards. Skipped by design: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in). Red: Check Changeset only, judged in ②. Governed surfaces in the file list: none; head repo equals base repo; 726 changed lines.

Implemented-by: claude/issue-21528-resume-plan-identity
Reviewed-by: e9b4084e-558f-5388-aae5-1c69d5d0d420

The Reviewed-by: value is CLAUDE_CODE_SESSION_ID as this isolated reviewer subagent read it from its environment; the seat that adopts this record is session_01DDZNkDVwPQnevTFcYE47H3. The dev was a mode:subagent run under that same seat, so Implemented-by: carries its branch, as the template prescribes.

VERDICT: PASS


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

Projects

None yet

2 participants