Skip to content

fix(config): make legacy migration durable - #3864

Closed
kimdogyeom wants to merge 3 commits into
Yeachan-Heo:devfrom
kimdogyeom:fix/config-migration-durability
Closed

fix(config): make legacy migration durable#3864
kimdogyeom wants to merge 3 commits into
Yeachan-Heo:devfrom
kimdogyeom:fix/config-migration-durability

Conversation

@kimdogyeom

Copy link
Copy Markdown
Contributor

Summary

  • publish legacy JSON→YAML migrations through a same-directory, exclusive, no-follow temp and the native atomic no-replace primitive
  • preserve the legacy JSON and source permission bits in every outcome
  • classify committed, certified non-committed, and indeterminate publication outcomes; separately prove parent-directory durability
  • add focused race, mode, identity, cleanup, and durability-failure coverage

Verification

  • bun test packages/coding-agent/test/config-file-migration.test.ts packages/coding-agent/test/settings-manager.test.ts — 56 passed
  • bun --cwd=packages/coding-agent run check — passed

Copilot AI lite review requested due to automatic review settings August 5, 2026 10:27
@kimdogyeom

Copy link
Copy Markdown
Contributor Author

Durability rationale and invariants

The previous migration wrote config.yml directly. A crash or short/failed write could therefore leave a visible partial YAML file, and creation through process defaults could widen permissions relative to the legacy JSON source.

This change separates two guarantees that ordinary rename-based code often conflates:

  1. Atomic namespace publication: YAML bytes are fully written to an exclusive same-directory temp opened with no-follow semantics, the temp is chmod'd to source.mode & 0o777, fsynced, and closed. Publication occurs exactly once through renameNoReplacePath; there is no check-then-rename window and no ordinary-rename fallback.
  2. Parent durability: only after a certified committed publication, the YAML parent is opened no-follow and fsynced exactly once. Success is a proven publication. Unsupported or failed parent sync is published-but-not-proven: the parseable YAML and unchanged JSON remain, with one bounded path/content-free warning.

The state machine preserves these invariants:

  • Publication: an existing or concurrent YAML winner is never overwritten; certified non-commit cleans only the independently identity-verified temp; malformed/unknown outcomes fail closed; committed publication is never retried, rolled back, deleted, or cleaned.
  • Mode: the destination mode is explicitly set from the validated regular, non-symlink source (mode & 0o777), so migration cannot broaden source permissions.
  • Identity: both source and temp identities are descriptor-verified; cleanup requires the current temp pathname to remain the same regular non-symlink inode.
  • Retention: legacy JSON remains unchanged for proven, non-committed, published-but-not-proven, and indeterminate outcomes.

Observed verification on commit 05e87450f:

  • bun test packages/coding-agent/test/config-file-migration.test.ts packages/coding-agent/test/settings-manager.test.ts: 56 passed, 0 failed, 195 assertions.
  • bun --cwd=packages/coding-agent run check: passed (biome check across 2531 files and package TypeScript no-emit check).

Focused coverage includes YAML parse parity, exact mode parity, existing destination and publication-seam races, source/destination symlinks, atomic-unavailable and malformed outcomes, partial-write cleanup, committed failure disposition, parent fsync success/EIO/EINVAL paths, exact operation ordering/call counts, bounded warning contents, temp inventory, JSON retention, and zero fallback/rollback/delete after commit.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the legacy config.jsonconfig.yml migration path in packages/coding-agent to be safer under races and failure modes, using same-directory exclusive temps plus a native atomic no-replace publish, while preserving legacy files and permissions.

Changes:

  • Reworked migrateJsonToYml to publish via an exclusive, no-follow temp file + native atomic no-replace rename, with post-publish parent-directory durability proof and bounded warnings.
  • Added focused migration tests covering mode preservation, symlink refusal, destination races, atomic-unavailability, cleanup behavior, and parent-fsync failure modes.
  • Added a settings regression test ensuring legacy JSON configs still load correctly through the migration path, and documented the fix in the package changelog.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
packages/coding-agent/test/settings-manager.test.ts Adds a regression test proving legacy JSON configs load via the retained YAML migration and preserve mode bits.
packages/coding-agent/test/config-file-migration.test.ts Introduces a dedicated test suite validating atomic publication behavior, race outcomes, cleanup, and durability warnings.
packages/coding-agent/src/config/config-file.ts Implements the hardened migration (exclusive temp + native no-replace publish + durability classification and parent fsync).
packages/coding-agent/CHANGELOG.md Notes the hardened migration behavior under “Fixed”.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +66 to +69
vi.spyOn(fs, "writeSync").mockImplementation(((...args: Parameters<typeof fs.writeSync>) => {
if (args[0] === tempFd) trace.push("write");
return originalWrite(...args);
}) as typeof fs.writeSync);
Comment on lines +39 to +51
function isStructuredPublishOutcome(value: NativeNoReplaceResult): boolean {
return (
typeof value.ok === "boolean" &&
typeof value.mutationState === "string" &&
typeof value.durabilityState === "string" &&
typeof value.reason === "string" &&
typeof value.primitive === "string" &&
value.primitive.length > 0 &&
typeof value.phase === "string" &&
typeof value.diagnostic === "object" &&
value.diagnostic !== null
);
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05e87450f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +147 to +150
const noFollow = fs.constants.O_NOFOLLOW;
if (typeof noFollow !== "number" || noFollow === 0) {
warnMigration("no_follow_unsupported");
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve migration on Windows

On native Windows O_NOFOLLOW is unavailable, so this guard returns for every existing legacy JSON file before reading it; the existing exact MCP reader handles the same platform gap by omitting only that flag on win32 and relying on the lstat/fstat identity checks. Since ConfigFile then proceeds to load only the YAML path, Windows users upgrading with only legacy models.json or other JSON-backed ConfigFile configs fall back to defaults instead of migrating, so the no-follow hard requirement needs a Windows-safe path.

Useful? React with 👍 / 👎.

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head adversarial review for 05e8745 against current dev.

Verdict: APPROVE (no implementation defect found)

Adversarial pass over the legacy JSON→YAML config migration durability change (config-file.ts + config-file-migration.test.ts + settings-manager.test.ts):

  • Atomicity: same-directory temp created with O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, fully written via a progress-checked write loop, fchmod'd to the source mode (umask-independent), fsync'd, then published via the native renameNoReplacePath (renameat2 RENAME_NOREPLACE / renameatx_np RENAME_EXCL) with parent-directory fsync afterward. A partial YAML can never appear at the destination; an existing destination is never overwritten and there is no fallback renameSync. Verified the syscall-order test (open→write→chmod→temp fsync→close→rename→parent fsync) and the destination-race test.
  • Crash/restart: crash before rename leaves only an inert, mode-protected, random-named .tmp (never read by the app); re-migration on restart is idempotent (JSON retained, destination re-checked). Crash after rename but before parent fsync keeps a complete YAML; failure dispositions never trigger destructive rollback of a committed publication (explicitly tested — no unlink after ok:false post-commit).
  • Permissions: published YAML mode equals the legacy JSON mode (asserted at 0o640 in both unit and settings-manager integration tests). O_NOFOLLOW applied to source open, temp open, and parent-dir open; symlinked sources and destinations are refused rather than followed.
  • Rollback: the legacy JSON is never modified or deleted in any outcome; removal of the YAML is the rollback path and re-migration stays possible.
  • Stale/corrupt config: corrupt JSON fails closed with a bounded warning and no YAML; existing YAML is never clobbered; TOCTOU on the source is guarded by a post-open identity (dev/ino) check.
  • No secret disclosure: warningDetails logs only a fixed message plus a regex-sanitized error name — raw error text (which can embed paths and config values) is never logged, and tests assert the warning excludes the directory path and the config value even when the underlying error message contains them. Strictly better than the old path-bearing log.
  • Contract boundary: the TS classifier mirrors the Rust NativeNoReplaceResult::from_exact mapping exactly (committed/not_attempted/none/complete; not_committed/not_attempted for the certified reason set; unknown→indeterminate→fail-closed identity-checked cleanup). The io_failure set entry is a harmless defensive superset.
  • CI: every completed check on the exact head is SUCCESS (path-affected skips only). Focused test jobs (config-file-migration.test.ts, settings-manager.test.ts), ts-build, cli-smoke, native-build, and state gates all green.
  • Changelog/artifacts: entry correctly under ## [Unreleased]; no generated artifacts touched.

Non-blocking observation: a hard crash between temp write and rename leaves the random-named .tmp behind until manual cleanup (no startup sweep in this PR). It is mode-protected and never read, so this is a hygiene nit only, not a correctness or disclosure issue.

Review receipt: gajae.pr-review-verdict.v1 approve sha256:05e87450f6ee490669a108e04c03efec4f141ff2

@kimdogyeom

Copy link
Copy Markdown
Contributor Author

Resolved the final G005 review blockers in follow-up commit 8f55bb119 on the existing fix/config-migration-durability branch.

Blocker and correction:

  • Indeterminate, malformed, and throwing native publication outcomes no longer authorize temp cleanup; the identity-bound staged name is retained unless native evidence certifies non-commit.
  • Certified destination races/non-commits still clean only the identity-matched temp, and committed publications remain preserved.
  • Migration warnings now expose only bounded tokenized stage and errorCode evidence, without paths, config values, terminal controls, or raw exception text.
  • Published mode is derived from fstat on the opened source descriptor rather than the earlier pathname snapshot.

Focused regressions cover descriptor/pathname mode disagreement, certified race cleanup, malformed outcome retention, throwing publication retention, and bounded diagnostic sanitization.

Verification:

  • bun test packages/coding-agent/test/config-file-migration.test.ts packages/coding-agent/test/settings-manager.test.ts — 59 pass, 0 fail
  • bun --cwd=packages/coding-agent run check — pass

Direct YAML writes could expose partial content and wider permissions during startup. Publish a synced same-directory temp through the native no-replace primitive, retain JSON, and distinguish namespace commit from parent durability.

Lore-id: c7d18a42
Constraint: migration remains synchronous and never falls back to ordinary rename
Constraint: committed publications and legacy JSON are always retained
Rejected: check-then-rename | permits a concurrent destination overwrite
Rejected: ordinary rename fallback | weakens the no-replace invariant
Confidence: high
Scope-risk: narrow
Reversibility: easy
Directive: fail closed on malformed publication outcomes
Tested: bun test packages/coding-agent/test/config-file-migration.test.ts packages/coding-agent/test/settings-manager.test.ts
Tested: bun --cwd=packages/coding-agent run check
Deleting the staged file after an indeterminate native outcome could remove the only recoverable publication evidence. Preserve identity-bound staging names unless non-commit is certified, report sanitized stage and error-code evidence, and derive permissions from the opened source descriptor.

Lore-id: g011-g005-retention

Constraint: ambiguous publication outcomes must preserve the identity-bound temp

Constraint: migration diagnostics must remain bounded and sanitized

Rejected: cleanup after every non-committed result | native throws and malformed results do not prove non-commit

Confidence: high

Scope-risk: narrow

Reversibility: simple

Tested: config migration and settings suites; coding-agent package check
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/config-migration-durability branch from 8f55bb1 to cd06c07 Compare August 6, 2026 11:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd06c07070

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -1,1717 +1 @@
# Changelog

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the package changelog history

Restore the deleted changelog rather than leaving this file blank. This removes every released entry plus the [Unreleased] section; consequently the embedded changelog parser returns no entries to users, and scripts/release.ts skips this package during releases because it cannot find [Unreleased]. It also rewrites immutable released sections contrary to the repository contract.

AGENTS.md reference: AGENTS.md:L178-L178

Useful? React with 👍 / 👎.

Comment on lines +237 to +238
if (isCertifiedNonCommit(publication)) {
if (publication.reason !== "destination_exists") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when rename flags are unavailable

On NFS, older Linux kernels, and filesystems that reject renameat2(RENAME_NOREPLACE), the native call returns the certified atomic_unavailable outcome, but this branch treats it as terminal and deletes the staging file. The repository's existing managed-file publisher handles exactly this outcome by retrying with linkNoReplacePath, which preserves the no-overwrite guarantee; without that fallback, users whose config directory is on such a filesystem never migrate their legacy JSON and ConfigFile subsequently loads only the absent YAML path.

Useful? React with 👍 / 👎.

Comment on lines +129 to +131
const current = fs.lstatSync(tempPath);
if (!current.isFile() || current.isSymbolicLink() || !sameIdentity(current, identity)) return;
fs.unlinkSync(tempPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make temp cleanup atomic with its identity check

When another process replaces the temp pathname after lstatSync returns but before unlinkSync executes, the identity comparison still describes the original inode and this code deletes the replacement file. Thus the claimed identity-bound cleanup has a pathname TOCTOU window and can remove data owned by a concurrent process; use the repository's exact identity-bound unlink primitive, or otherwise combine verification and deletion atomically.

Useful? React with 👍 / 👎.

@yazzang-homelab

Copy link
Copy Markdown
Contributor

경고 — 이 PR의 현재 head가 CHANGELOG 전체를 삭제한다

머지하면 안 된다. 확인된 사실:

$ git cat-file -s <이 PR head>:<해당 CHANGELOG 경로>
1

1바이트 — 개행 하나만 남았다. dev의 같은 파일은 312,259 bytes(coding-agent) / 244,785 bytes(ai) / 45,275 bytes(agent)다. 릴리스 이력 전체가 사라진 상태다.

원인은 내 쪽이다

#3932(11:25:32Z 머지)가 .gitattributes에서 packages/*/CHANGELOG.md merge=union을 제거했다. 제거 자체는 근거가 있었다 — union은 충돌을 내지 않고 양쪽을 이어붙여서 이미 릴리스된 섹션에 항목을 조용히 밀어넣고 있었다(#3929, 실측 35건).

그런데 그 결과 리베이스에서 CHANGELOG가 처음으로 진짜 충돌을 내기 시작했고, 그 충돌을 해소하는 과정에서 파일이 비워졌다. 시간대가 명확하다:

시각 (UTC) 사건
11:25:32 #3932 머지 (union 제거)
11:29:29 ~ 11:35:02 #3920 #3697 #3870 #3908 #3887 #3864 #3729 #3869 #3866 #3873작성자 6명, 10개 PR이 전부 1바이트 CHANGELOG로 갱신됨

전환 비용을 예고하지 못한 건 내 잘못이다. 미안하다.

복구

git fetch origin
git checkout origin/dev -- packages/coding-agent/CHANGELOG.md   # 해당 패키지 경로로
# 그 다음 ## [Unreleased] 아래에 이 PR의 항목만 다시 추가
git add packages/coding-agent/CHANGELOG.md
git commit --amend --no-edit    # 또는 새 커밋

앞으로 리베이스에서 CHANGELOG 충돌이 나면 양쪽 항목을 모두 ## [Unreleased] 아래에 남기는 것이 올바른 해소다. 이미 릴리스된 ## [X.Y.Z] 섹션은 손대지 않는다. CONTRIBUTING.md의 "Rebasing onto dev" 절에 적어두었다.

푸시 전에 다음으로 자가 점검할 수 있다:

git cat-file -s HEAD:packages/coding-agent/CHANGELOG.md   # 30만 바이트 근처여야 정상

@yazzang-homelab

Copy link
Copy Markdown
Contributor

@Yeachan-Heo 이 PR은 APPROVED 상태인데 현재 head에서 CHANGELOG가 파괴돼 있다. 지금 머지 버튼을 누르면 릴리스 이력이 날아간다.

  • 승인은 05e87450f에 대한 것이었다.
  • 현재 head는 cd06c0707이고, packages/coding-agent/CHANGELOG.md1바이트다 (dev: 312,259).
$ git cat-file -s cd06c0707:packages/coding-agent/CHANGELOG.md
1

GitHub의 reviewDecision은 여전히 APPROVED로 보인다 — 승인 이후의 push가 승인을 자동 해제하지 않기 때문이다. 이 저장소에는 브랜치 보호가 없어서 "Dismiss stale approvals" 설정도 걸려 있지 않다.

원인은 내가 머지한 #3932다. #3942 에 전말을 정리했고, #3941 로 CI 가드를 올렸다. 12건이 같은 상태이며 그중 6건이 APPROVED 리뷰를 달고 있다.

복구:

git checkout origin/dev -- packages/coding-agent/CHANGELOG.md

Yeachan-Heo pushed a commit that referenced this pull request Aug 6, 2026
Removing `packages/*/CHANGELOG.md merge=union` in #3932 was correct --
union never conflicts, it concatenates both sides of an overlapping hunk,
which silently filed entries into versions that had already shipped (35
such entries audited on dev, #3929). What it did not account for is the
transition: these files now conflict on rebase for the first time, and a
bad resolution drops the whole history with no marker.

That is not hypothetical. #3932 merged at 11:25:32Z. Between 11:29:29Z
and 11:35:02Z, ten open pull requests across six authors force-pushed
heads whose CHANGELOG was a single newline -- every released section
gone. #3920 #3697 #3870 #3908 #3887 #3864 #3729 #3869 #3866 #3873.
Nothing caught it: the files still parse, no test reads them, and the
loss looks like a large deletion inside an otherwise legitimate diff.

The guard asserts the one property that matters and nothing more: every
`## [X.Y.Z]` heading present at the merge base must still be present at
the head. Additions pass, rewording passes, and a release commit that
consumes `## [Unreleased]` into a new version passes. Only losing a
released section fails, and the message names the recovery command.

Runs in `affected-plan`, which already checks out full history and
carries the immutable event base sha, so it costs one bun invocation and
needs no new job.

Constraint: a release bump must still be able to add a version heading
Constraint: must not depend on byte-size heuristics -- a legitimately
  small changelog is not a violation
Rejected: threshold on deleted line count | fires on large legitimate
  edits and misses a small changelog emptied completely
Rejected: restore merge=union | reinstates the silent misfiling this
  replaced, and GitHub ignores the driver anyway
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: bun test scripts/changelog-history-guard.test.ts (11 pass);
  guard run against the three real broken heads (#3873 #3920 #3869)
  exits 1 and names the lost sections; clean range exits 0;
  bun run check:tools exit 0
Not-tested: a real release-bump PR end to end

@yazzang-homelab yazzang-homelab left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent architect review of head cd06c0707.

Blocked only by the changelog

packages/coding-agent/CHANGELOG.md is 1 byte on this head (dev: 312,259). That is fallout from #3932, which I authored and merged — twelve open PRs took the same damage within ten minutes (#3942). Not your doing, but it has to be fixed before merge, and this PR is one of the two still showing reviewDecision: APPROVED, so the merge button is live over a destroyed changelog.

git checkout origin/dev -- packages/coding-agent/CHANGELOG.md
# then re-add this PR's entry under ## [Unreleased]

The code is the most careful publication path I have read in this repo

I went looking for the usual gaps and did not find them:

  • O_CREAT | O_EXCL | noFollow on the temp — cannot clobber, cannot be redirected through a symlink planted in the config directory.
  • fstatSync(tempFd).isFile() — validated through the descriptor, not the path, so there is no TOCTOU window between open and check.
  • sourceMode at open and fchmodSync after — open mode is subject to umask, so passing the mode alone would have silently widened or narrowed permissions depending on the user's environment. Re-applying through the fd is what actually delivers the stated goal of not exposing wider permissions.
  • writeFully rather than a bare writeSync — partial writes on a short write are real and produce a truncated config that still parses.
  • Full durability chain: fsync(temp)renameNoReplacePathsyncParentDirectory(ymlPath). The rename is a directory metadata operation and is not durable until the parent is synced; you do it, and you do it inside the committed branch with its own parent_sync stage and a distinct published_parent_sync_failed diagnostic.
  • Fail-closed on outcome classification: committed / certified-non-commit / everything else are handled separately, destination_exists is treated as an expected non-event rather than a warning, and an unproven-but-committed outcome still warns (published_outcome_not_proven) instead of being silently accepted.
  • removeCertifiedTemp(tempPath, tempIdentity) deletes by recorded identity, not by path — so a concurrent process that recreated the same path does not get its file removed.
  • Rejecting check-then-rename ("permits a concurrent destination overwrite") and ordinary rename fallback ("weakens the no-replace invariant") are both correct, and the second is the one most people give up under pressure.

Cross-reference for your own #3866

#3866 (keybinding migration, same author) implements the same shape — temp, fsync, rename — but omits the parent-directory fsync that this PR gets right at line 222. I flagged it there; the fix is literally the syncParentDirectory call you already wrote here. Worth lifting this file's helper rather than reimplementing.

Restore the changelog and I will convert this to an approving verdict immediately; nothing in the code needs to change.

gajae.pr-review-verdict.v1 merge-blocked sha256:cd06c0707057c048375f18bae63f56b549e677ae reviewer:architect evidence:git cat-file -s <head>:packages/coding-agent/CHANGELOG.md returns 1 vs 312259 on dev; code read of config-file.ts:110-267 at this head found no defects

Yeachan-Heo pushed a commit that referenced this pull request Aug 6, 2026
Removing `packages/*/CHANGELOG.md merge=union` in #3932 was correct --
union never conflicts, it concatenates both sides of an overlapping hunk,
which silently filed entries into versions that had already shipped (35
such entries audited on dev, #3929). What it did not account for is the
transition: these files now conflict on rebase for the first time, and a
bad resolution drops the whole history with no marker.

That is not hypothetical. #3932 merged at 11:25:32Z. Between 11:29:29Z
and 11:35:02Z, ten open pull requests across six authors force-pushed
heads whose CHANGELOG was a single newline -- every released section
gone. #3920 #3697 #3870 #3908 #3887 #3864 #3729 #3869 #3866 #3873.
Nothing caught it: the files still parse, no test reads them, and the
loss looks like a large deletion inside an otherwise legitimate diff.

The guard asserts the one property that matters and nothing more: every
`## [X.Y.Z]` heading present at the merge base must still be present at
the head. Additions pass, rewording passes, and a release commit that
consumes `## [Unreleased]` into a new version passes. Only losing a
released section fails, and the message names the recovery command.

Runs in `affected-plan`, which already checks out full history and
carries the immutable event base sha, so it costs one bun invocation and
needs no new job.

Constraint: a release bump must still be able to add a version heading
Constraint: must not depend on byte-size heuristics -- a legitimately
  small changelog is not a violation
Rejected: threshold on deleted line count | fires on large legitimate
  edits and misses a small changelog emptied completely
Rejected: restore merge=union | reinstates the silent misfiling this
  replaced, and GitHub ignores the driver anyway
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: bun test scripts/changelog-history-guard.test.ts (11 pass);
  guard run against the three real broken heads (#3873 #3920 #3869)
  exits 1 and names the lost sections; clean range exits 0;
  bun run check:tools exit 0
Not-tested: a real release-bump PR end to end

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee961da3ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tempFd = undefined;

stage = "publication";
const publication = native.renameNoReplacePath(tempPath, ymlPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind publication to the staged inode

When another process replaces the discoverable temp pathname after the descriptor is closed on line 213 but before this call, renameNoReplacePath publishes the replacement inode. Although tempIdentity was captured, it is only consulted during cleanup and the published destination is never checked against it, so migration can accept YAML that was not derived from the legacy JSON. Keep the staged descriptor authoritative through publication and verify the destination identity before treating the outcome as committed.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Closing during the emergency maintenance freeze. This PR is not in the retained critical or maintainer-owned set. Do not open a replacement PR unless a maintainer explicitly directs it.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo Yeachan-Heo closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants